What is the advantage of Atomic classes over synchronized?
Atomic classes and synchronized are two different approaches to thread safety:
Junior Level
Basic Understanding
Atomic classes and synchronized are two different approaches to thread safety:
| Approach | Principle | Analogy |
|---|---|---|
| synchronized | Pessimistic: “Contention will happen — I’ll lock immediately” | One person enters the room, others wait in line |
| Atomic | Optimistic: “Contention is unlikely — I’ll try fast” | Everyone tries to enter at once, whoever succeeds — updated |
Simple Example
// synchronized — pessimist
public class SynchronizedCounter {
private int count = 0;
public synchronized void increment() {
count++; // Only one thread at a time, others wait
}
}
// Atomic — optimist
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // Fast CAS attempt, if it fails — try again
}
}
When to use which
| Situation | Choice | Why |
|---|---|---|
| Simple counter | Atomic | Faster (CAS ~5ns vs synchronized ~1000ns — no context switch), no deadlock |
| Compound operation | synchronized | Need to lock multiple fields |
| Flag | AtomicBoolean | Lighter than synchronized |
| Updating multiple fields | synchronized | Atomic only works on one field |
Middle Level
Why is Atomic faster?
1. No Context Switch
synchronized (under contention):
Thread → BLOCKED → OS parks → Context Switch → ~1000-5000 ns
Atomic (CAS):
Thread → RUNNABLE → CAS attempt → ~5-10 ns (no context switch)
With synchronized, the thread transitions to BLOCKED state, which requires calling the OS kernel and context switching — thousands of CPU cycles.
Atomic uses Spin-waiting: the thread doesn’t sleep, but retries the CAS in a loop. This is faster because it doesn’t transfer control to the operating system (no context switch).
2. Progress Guarantees
| Problem | synchronized | Atomic |
|---|---|---|
| Deadlock | Possible | Impossible (for individual Atomic operations; but business logic based on multiple Atomic can still hang) |
| Priority Inversion | Possible | Impossible |
| Forgotten unlock | N/A (automatic) | N/A |
// synchronized — deadlock possible
synchronized(lockA) {
synchronized(lockB) { // If another thread did the opposite → deadlock
// ...
}
}
// Atomic — deadlock impossible
counter1.incrementAndGet(); // Always completes
counter2.incrementAndGet(); // Always completes
How the CAS Cycle Works
// Simplified incrementAndGet implementation
public final int incrementAndGet() {
for (;;) { // Spin cycle
int current = get(); // 1. Read volatile value
int next = current + 1; // 2. Compute new
if (compareAndSet(current, next)) { // 3. CAS
return next; // Success!
}
// CAS failed — someone else updated
// Go to next iteration (without blocking!)
}
}
When Atomic is WORSE than synchronized?
1. High Contention
100 threads simultaneously doing incrementAndGet():
- CAS constantly fails
- Threads spin in cycles, burning CPU at 100%
- Almost no progress
In this case synchronized may be more effective:
- Threads queue up
- CPU is not wasted on empty cycles
2. Compound Operations
// Atomic won't help atomically update x and y
AtomicInteger x = new AtomicInteger(0);
AtomicInteger y = new AtomicInteger(0);
// NOT safe!
void swap() {
int temp = x.get();
x.set(y.get()); // Another thread may see intermediate state
y.set(temp);
}
// synchronized — safe
synchronized(lock) {
int temp = x;
x = y;
y = temp;
}
3. Long Operations
// BAD: long operation in CAS cycle
atomicRef.updateAndGet(current -> {
// Long computation — CPU will burn under contention!
return expensiveTransformation(current);
});
// GOOD: long operation in synchronized
synchronized(lock) {
ref = expensiveTransformation(ref); // Other threads wait, CPU doesn't burn
}
Senior Level
Under the Hood: Bus Contention
Even when CAS succeeds, it generates traffic on the data bus:
Thread on Core 1 does CAS:
1. lock cmpxchg [memory], new_value
2. Lock memory bus
3. Invalidate cache lines on all other cores
4. Wait for Ack from all cores
5. Unlock bus
Many Atomic variables can “clog” the server’s memory bus.
Lock-free vs Wait-free vs Obstruction-free
| Term | Guarantee | Example |
|---|---|---|
| Lock-free | At least one thread completes in finite time | AtomicInteger |
| Wait-free | Every thread completes in finite steps | LongAdder.add() |
| Obstruction-free | Thread completes if others don’t interfere | Some STM |
synchronized — none of the above (blocking):
- Thread can be blocked indefinitely (deadlock, starvation)
Performance: Break-Even Point
Thread count | Atomic (ns/op) | synchronized (ns/op)
───────────────────────────────────────────────────────
1 | 5 | 10
2 | 10 | 15
4 | 20 | 20
8 | 50 | 25
16 | 200 | 30
32 | 1000+ | 40
64 | Spin! | 50
Numbers are approximate, depend on CPU/JVM. Measure on your hardware via JMH.
Break-even point: ~4-8 threads for simple operations. After that, synchronized becomes faster.
LongAdder as an Evolution of the Atomic Idea
// Problem: AtomicInteger at 100 threads → CAS contention
AtomicInteger counter = new AtomicInteger(0);
// Solution: LongAdder distributes across cells
LongAdder counter = new LongAdder();
counter.increment(); // Each thread to its own cell
long total = counter.sum(); // Sums all cells
LongAdder internally:
// Base value + cell array
volatile long base;
volatile Cell[] cells; // Each thread hashes to its own cell
// @Contended padding — avoid False Sharing
static final class Cell {
@Contended
volatile long value;
}
Diagnostics
Thread Dumps
jstack <pid>
# If synchronized is slow — we see BLOCKED threads
"worker-1" BLOCKED (on object monitor)
- waiting to lock <0x000...>
# If Atomic is slow — no BLOCKED, threads in RUNNABLE
"worker-1" RUNNABLE
at AtomicInteger.compareAndSet(...)
// Thread spinning in CAS cycle
-XX:+PrintAssembly
# synchronized → heavy OS calls
call runtime_monitor_enter
# Atomic → single instruction
lock cmpxchg dword ptr [rax], rcx
Best Practices
- Atomic for simple operations under low/medium contention
- synchronized for compound operations or under high contention
- LongAdder for counters under extreme load
- Avoid Atomic for long computations — CAS cycle will burn CPU
- Monitor: no BLOCKED in dump, but system is slow → look for hot Atomic
- Back-off:
Thread.onSpinWait()as a CPU hint on CAS retry
Interview Cheat Sheet
Must know:
- Atomic = optimistic approach (try fast, retry on conflict), synchronized = pessimistic (lock immediately)
- Atomic uses CAS cycle (spin-waiting): thread doesn’t sleep, retries — no context switch
- synchronized transitions thread to BLOCKED (~1000-5000 ns), Atomic stays in RUNNABLE (~5-10 ns)
- Break-even point: ~4-8 threads for simple operations — after that synchronized may become faster
- Atomic guarantees lock-free progress (at least one thread completes), synchronized can deadlock
- Atomic does NOT help with compound operations (updating 2+ fields) — synchronized is needed there
- Under high contention, CAS constantly fails, threads burn CPU at 100% — LongAdder or synchronized is better
- LongAdder evolves the Atomic idea: distributes counting across cells (per-thread), sums on read
Frequent follow-up questions:
- Why is Atomic faster under low contention? — No context switch or OS call, one lock cmpxchg instruction
- What is lock-free vs wait-free? — Lock-free: at least one thread completes; wait-free: every thread completes in finite steps (LongAdder.add)
- What happens at the CPU level during CAS? — Memory bus lock → invalidate cache lines on all cores → wait for Ack → unlock
- When is synchronized better than Atomic? — High contention (8+ threads), compound operations, long computations
Red flags (do NOT say):
- “Atomic is always faster than synchronized” — no, break-even point is ~4-8 threads
- “Atomic uses locks” — no, it’s a lock-free CAS cycle
- “Deadlock is impossible in any code with Atomic” — individual Atomic operations are safe, but business logic across multiple Atomic can still hang
Related topics:
- [[10. How do AtomicInteger, AtomicLong work]]
- [[12. What is Thread Pool]]
- [[18. What is deadlock]]