volatile vs synchronized vs atomic — when each?
Hardvolatile makes one variable's writes visible to other threads but doesn't make count++ atomic. Atomic classes do single-variable read-modify-write with CAS, no lock. synchronized gives mutual exclusion plus visibility, for rules that span several fields.
How it works
- The problem. Threads may cache values in registers or CPU caches, and the compiler and CPU may reorder instructions. Without a happens-before link, one thread may never see another's write.
volatile- Every read sees the latest write to that variable, and a write also publishes everything the writer did before it.
- No locking, so no blocking.
- It does not make compound actions atomic:
count++is read, add, write, and two threads can interleave.
AtomicInteger,AtomicLong,AtomicReference…- Use compare-and-set (CAS) hardware instructions: "set to new value only if it's still the old value", retrying on failure.
incrementAndGet(),compareAndSet(),updateAndGet(),accumulateAndGet().- Safe for one variable. Two atomics updated one after the other are not one atomic step.
synchronized- Only one thread at a time holds the monitor; others block.
- Leaving the block publishes all writes to the next thread that enters with the same lock.
- Reentrant. Needed when an invariant links several fields (e.g.
balanceandhistorymust change together).
Example
class Downloader {
private volatile boolean cancelled; // one writer, many readers
private final AtomicLong bytes = new AtomicLong();
private final List<String> failed = new ArrayList<>();
private int failures;
void cancel() { cancelled = true; } // seen by worker threads promptly
void onChunk(int size) {
if (cancelled) return;
bytes.addAndGet(size); // atomic, no lock
}
synchronized void onFailure(String url) { // two fields must stay in step
failed.add(url);
failures++;
}
}
// Lost updates with volatile alone
class BadCounter { volatile int n; void hit() { n++; } } // 2 threads × 1M hits → often < 2MEdge cases
- Plain
longanddoublewrites may be split into two 32-bit halves on some JVMs. Declaring themvolatilemakes each read and write whole. volatileon an array or object reference covers the reference, not the elements or fields inside. UseAtomicIntegerArrayor immutable objects.- CAS loops can spin under heavy contention.
LongAdderspreads updates across cells and sums them on read, trading exact snapshots for throughput. - Before Java 24, a virtual thread blocking inside
synchronizedpinned its carrier thread. JEP 491 (Java 24) removed that limitation; on Java 21 preferReentrantLockaround blocking calls in virtual threads.
Common mistakes
- Marking a counter
volatileand assuming++is now thread-safe. if (map.get(k) == null) map.put(k, v)on separate atomic operations: each step is safe, the combination isn't. UseputIfAbsent/computeIfAbsent.- Synchronizing on different lock objects in different methods that guard the same data.
Likely follow-up
"Is double-checked locking safe?" Only if the field is volatile. Without it, another thread can see a non-null reference to an object whose constructor writes aren't visible yet.
Get every deep dive in the app
Coming soon to the App StoreComing soon to Google Play