Java's concurrency model is large. Most of it is never needed. Here is the part that comes up repeatedly in real production systems.
The memory model in one sentence
The Java Memory Model guarantees that if thread A writes a value and thread B reads it, B will see A's write only if there is a happens-before relationship between the write and the read.
Without that relationship, B may see a stale cached value. synchronized, volatile, and the java.util.concurrent locks all establish happens-before.
volatile
volatile ensures a write is immediately visible to other threads. It does not provide atomicity.
private volatile boolean running = true;
// Thread A
running = false;
// Thread B — will eventually see false because of volatile
while (running) { doWork(); }
Use volatile for single-field flags. Use AtomicBoolean if you need compare-and-set.
synchronized
synchronized provides both mutual exclusion and happens-before. Every synchronized block is a happens-before boundary.
Keep synchronized blocks short. Hold locks only long enough to protect the critical section. The longer a lock is held, the more contention.
ReentrantLock
ReentrantLock does everything synchronized does, with extras:
tryLock()— attempt to acquire without blockingtryLock(timeout, unit)— attempt with a timeoutlockInterruptibly()— can be interrupted while waitingReentrantReadWriteLock— separate read and write locks, allowing concurrent reads
Use ReentrantLock when you need these features. Use synchronized otherwise — it is simpler and the JVM optimizes it well.
Executors over raw threads
Creating threads directly is rarely the right choice. The ExecutorService API manages thread pools, handles lifecycle, and separates task submission from execution policy.
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());
pool.shutdown();
For Java 21+, consider virtual threads for I/O-heavy workloads. They are cheap enough to create per-task.
Deadlock
Deadlocks are caused by circular lock acquisition. If you must acquire multiple locks, always acquire them in the same order everywhere. tryLock with a timeout is a practical fallback.
The one rule
If a field is accessed by more than one thread, something must provide visibility. If it can be written by more than one thread, something must provide atomicity. Pick the right tool based on what you actually need.