synchronized, volatile, Atomics and Locks
Shared mutable state needs two guarantees: atomicity (a compound action is not interleaved) and visibility (a write by one thread becomes visible to others, in a defined order). Each tool provides a different mix.
| Tool | Atomicity | Visibility / ordering | Blocking |
|---|---|---|---|
synchronized | Yes (critical section) | Yes | Yes |
volatile | Only single reads/writes | Yes | No |
| Atomic classes (CAS) | Yes, single variable | Yes | No (lock-free) |
ReentrantLock & friends | Yes | Yes | Yes (with options) |
Definition
These are tools that keep threads from stepping on each other. synchronized and locks let one thread at a time into a section of code. volatile makes a change visible to other threads. Atomics update a single value safely without a lock.
Why it exists
When two threads change the same data, updates can be lost or one thread can see stale values. Each tool guarantees a specific kind of safety so you can reason about shared data.
How it works
synchronized
Every object has an intrinsic lock (monitor). synchronized acquires it on entry and releases it on exit (normal or exceptional). It is reentrant: a thread holding the lock can enter other synchronized blocks on the same object.
class Counter {
private int count;
public synchronized void increment() { count++; } // locks 'this'
public synchronized int get() { return count; } // reads need the same lock for visibility
}
private final Object lock = new Object(); // prefer a private lock object over 'this'
void work() { synchronized (lock) { ... } }
static synchronized void util() { ... } // locks Counter.class
Why a private lock: if you synchronise on this, outside code can also synchronise on your object and interfere or deadlock with you.
JVM implementation detail (useful for "how does synchronized perform?" questions): uncontended locks are cheap (HotSpot's lightweight locking via CAS on the object header); contended locks inflate to heavyweight monitors that park threads. Biased locking was removed (disabled in JDK 15, removed in 18). Before Java 24, a virtual thread blocked in synchronized pinned its carrier thread; JDK 24 (JEP 491) largely removed that limitation (see Virtual threads).
wait()/notify()/notifyAll() coordinate threads on a monitor condition; always wait in a loop and notify while holding the lock. Prefer higher-level utilities (BlockingQueue, Condition, CountDownLatch) over raw wait/notify.
volatile
A volatile field gives visibility and ordering: a write happens-before every subsequent read of that field, and it prevents certain reorderings. It does not make compound operations atomic.
private volatile boolean running = true; // correct use: a stop flag
void stop() { running = false; }
void loop() { while (running) { ... } } // without volatile the JIT may hoist the read: infinite loop
private volatile int hits;
void hit() { hits++; } // BROKEN: read-modify-write is three steps; updates are lost
Good uses: one-writer flags, safely publishing an immutable object by reference (volatile Config config), the double-checked locking field. For counters use AtomicInteger or LongAdder. Details of why in Memory model.
// Double-checked locking is correct only with volatile
class Lazy {
private static volatile Heavy instance;
static Heavy get() {
Heavy h = instance;
if (h == null) {
synchronized (Lazy.class) {
h = instance;
if (h == null) instance = h = new Heavy();
}
}
return h;
}
}
// Simpler: the initialization-on-demand holder idiom, thread-safe through class initialization
class LazyHolder { private static class H { static final Heavy I = new Heavy(); } static Heavy get() { return H.I; } }
Atomics and CAS
Compare-and-swap (CPU instruction) updates a variable only if it still holds the expected value, so threads can update without locking.
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
counter.updateAndGet(x -> Math.min(x + 1, 100)); // retried in a CAS loop until it succeeds
counter.compareAndSet(5, 6);
AtomicReference<Config> ref = new AtomicReference<>(Config.defaults());
ref.updateAndGet(c -> c.withTimeout(Duration.ofSeconds(2))); // lock-free copy-on-write of an immutable value
- The update function may be called several times under contention; keep it pure.
- ABA problem: a value changes A → B → A and a CAS wrongly succeeds. Solve with
AtomicStampedReference/AtomicMarkableReference(version stamp) when the intermediate change matters. - Under heavy contention CAS loops spin and waste CPU.
LongAdderkeeps striped cells and sums them on read: much better for high-contention counters and metrics (at the cost of a slightly stalesum()and more memory). UseAtomicLongwhen you need exact compare-and-set semantics. VarHandle(Java 9) exposes fine-grained access modes (getAcquire,setRelease) for advanced lock-free code.
ReentrantLock and Condition
private final ReentrantLock lock = new ReentrantLock();
void update() {
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // always in finally
}
}
boolean tryUpdate() throws InterruptedException {
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) { // bounded wait: avoids deadlock
try { /* ... */ return true; } finally { lock.unlock(); }
}
return false;
}
Compared with synchronized:
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Release | Automatic | Manual in finally |
| Try / timed / interruptible acquire | No | tryLock, tryLock(timeout), lockInterruptibly |
| Fairness option | No | new ReentrantLock(true) (longest-waiting first; lower throughput) |
| Multiple conditions | One wait set | Many Conditions (newCondition()) |
| Introspection | None | isLocked, getQueueLength, etc. |
A bounded buffer with two conditions:
class BoundedBuffer<T> {
private final Queue<T> q = new ArrayDeque<>();
private final int capacity;
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
BoundedBuffer(int capacity) { this.capacity = capacity; }
void put(T item) throws InterruptedException {
lock.lock();
try {
while (q.size() == capacity) notFull.await();
q.add(item);
notEmpty.signal();
} finally { lock.unlock(); }
}
T take() throws InterruptedException {
lock.lock();
try {
while (q.isEmpty()) notEmpty.await();
T item = q.remove();
notFull.signal();
return item;
} finally { lock.unlock(); }
}
}
In real code just use ArrayBlockingQueue; the example shows the mechanics.
Read-write locks and StampedLock
ReentrantReadWriteLock allows many readers or one writer: worthwhile only when reads dominate and the critical section is long enough to offset the overhead. It can starve writers or readers depending on mode, and lock downgrading (write → read) is supported while upgrading is not (deadlock).
StampedLock adds optimistic reads: read without locking, then validate that no write intervened.
class Point {
private final StampedLock sl = new StampedLock();
private double x, y;
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double cx = x, cy = y;
if (!sl.validate(stamp)) { // a writer got in: fall back to a real read lock
stamp = sl.readLock();
try { cx = x; cy = y; } finally { sl.unlockRead(stamp); }
}
return Math.hypot(cx, cy);
}
void move(double dx, double dy) {
long stamp = sl.writeLock();
try { x += dx; y += dy; } finally { sl.unlockWrite(stamp); }
}
}
StampedLock is not reentrant and does not support conditions; misuse (unlock with a wrong stamp) is easy.
Coordination utilities
| Utility | Purpose |
|---|---|
CountDownLatch(n) | One-shot: threads wait until the count reaches zero (start gate, "all workers done") |
CyclicBarrier(n) | Reusable: n threads wait for each other at a point, optionally running an action |
Semaphore(permits) | Limit concurrent access to a resource (connection limiter, bulkhead) |
Phaser | Flexible, dynamic multi-phase barrier |
Exchanger | Two threads swap objects |
BlockingQueue | Producer-consumer hand-off |
Semaphore limit = new Semaphore(20); // at most 20 concurrent calls to a fragile service
void call() throws InterruptedException {
limit.acquire();
try { remote.call(); } finally { limit.release(); }
}
System design considerations
- Hold locks for the shortest time and never across remote calls
- Document lock ordering to prevent deadlock
- Prefer immutability or thread confinement before reaching for locks
When to use it, and when not
Use it when
- Several threads update the same state and you must protect an invariant
- You need a thread-safe counter or flag (atomics, volatile)
- You need to coordinate start, finish or limits (latch, semaphore)
Think twice when
- A higher-level structure exists (ConcurrentHashMap, BlockingQueue) that already does it
- You hold a lock while calling unknown code or doing I/O
- The state could be confined to one thread or made immutable
Choosing
- Avoid sharing: confinement, immutability, message passing (
BlockingQueue, actors). - Simple state: an immutable snapshot in a
volatileorAtomicReference. - Counters/flags:
AtomicXxx,LongAdder,volatile. - Compound invariants over several fields: one lock (
synchronizedorReentrantLock) guarding all of them, always the same lock. - Lock-free structures only when measurements prove locks are the bottleneck.
Common mistakes
- Forgetting
unlock()infinally. - Different locks guarding the same data (no mutual exclusion at all).
- Using a
volatilecounter. - Holding locks while calling unknown code (callbacks) or doing I/O.
- Synchronising on strings, boxed values or other shared/cached objects.
- Unbounded waits without timeouts.
Interview questions
1. synchronized vs ReentrantLock?
ReentrantLock adds timed/interruptible/try acquisition, fairness and multiple conditions, but needs manual unlock in finally; synchronized is simpler and automatically released. Prefer synchronized unless you need those features.
2. What does volatile guarantee? Is volatile int count; count++ safe?
Visibility and ordering of reads/writes of that variable. count++ is not atomic; use AtomicInteger/LongAdder.
3. What is CAS and what is the ABA problem?
An atomic hardware compare-and-set. ABA is a value changing A→B→A so a CAS cannot detect the intermediate change; use stamped references.
4. AtomicLong vs LongAdder?
AtomicLong has a single contended cell (exact CAS operations); LongAdder stripes updates across cells to reduce contention, ideal for high-throughput counters where an approximate-at-read sum is fine.
5. How do you avoid deadlock with locks?
Global lock ordering, tryLock with timeouts, fewer nested locks, shorter critical sections, or restructure to message passing.
6. What is a reentrant lock?
A lock that the owning thread can acquire again without blocking; it keeps a hold count and releases when the count returns to zero.
7. CountDownLatch vs CyclicBarrier?
A latch is one-shot and releases waiters when the count hits zero; a barrier is reusable and makes all parties wait for each other.
8. Is double-checked locking safe?
Only if the field is volatile (Java 5+). Prefer the holder idiom or an enum.
9. Why is ReadWriteLock not always faster?
Acquiring read locks still updates shared lock state (cache-line contention); with short critical sections or frequent writes it can be slower than a plain lock.
Check yourself
1Is count++ on a volatile int thread-safe?
2What does compare-and-swap do?
3You need up to N threads inside a section at once. Which tool fits?
Key takeaways
- Atomicity and visibility are separate concerns; know what each primitive gives.
- Prefer immutability, confinement and high-level utilities over hand-rolled locking.
volatileis for flags and safe publication, not for compound updates.- Use
LongAdderfor hot counters andtryLocktimeouts to prevent deadlocks.
Finished this page? Track your progress (saved only in your browser).