On this page
Description
The previous skill was about starting and stopping threads. This one is about the hard part: what happens when two threads touch the same data. A counter that two threads increment, a settings object one thread updates while others read it, a cache filled by a background job. Java gives you two keywords for this, synchronized and volatile, and one rule underneath both of them, called happens-before.
Get them wrong and the bugs are nasty: a total that's a little too low, a loop that never sees its stop signal, an object that's seen half-built. They usually don't show up in tests on a laptop, and then they show up in production on a 64-core server. Understanding this skill is what lets you write concurrent code you can trust, and explain it in an interview.
Here's the path, from easiest to most technical:
- Why
count++from two threads loses updates. synchronized: what it locks, and the ways people lock the wrong thing.- Why reads need the lock too.
volatile: what it promises and what it doesn't.- Happens-before: the one rule behind all of it.
- Immutable objects, and the constructor that leaks
this.
Why count++ loses updates
count++ looks like one step, but it's three: read the value, add one, write it back. If two threads do it at the same moment, their steps can interleave like this:
This is a race condition: the result depends on the timing of the threads. Two threads each incrementing a plain counter a million times usually end well short of 2,000,000.
synchronized: one thread at a time
synchronized makes a block of code run by one thread at a time. Every Java object has a lock (also called its monitor); synchronized takes it on the way in and releases it on the way out, even if an exception is thrown:
private final Object lock = new Object();
private long count;
void increment() {
synchronized (lock) { // only one thread at a time gets past this line
count++;
}
}
You can also mark a whole method synchronized. An instance method then locks this, and a static one locks the class object (Counter.class).
The one rule to remember: only threads that lock the same object wait for each other. Most synchronized bugs are a different object being locked than the author thought:
- A field you reassign.
synchronized (requests) { requests++; }on anIntegerfield:requests++creates a newIntegerand puts it in the field, so threads lock whichever object happened to be there when they arrived. Two threads, two different locks, no protection. (The compiler even warns about locking a boxed value.) - A new object each time.
synchronized (new Object())protects nothing. - Static versus instance. A
static synchronizedmethod (locks the class) and an instancesynchronizedmethod (locksthis) can run at the same time. And instance locks don't protect astaticfield shared by every instance. - Shared or public objects. A string literal or a small boxed number is shared across the whole JVM, and a public lock object can be grabbed by any code.
The safe habit: lock a private final object, and use that same lock for every access to the data it protects.
A thread that already holds a lock can take it again (locks are reentrant), so one synchronized method can call another on the same object without blocking itself.
Reads need the lock too
This class looks safe because its setter is synchronized:
class Config {
private int timeoutMs = 1000;
synchronized void setTimeout(int ms) { timeoutMs = ms; }
int timeout() { return timeoutMs; } // no lock
}
But synchronized isn't only about taking turns. It's also how a change made by one thread becomes visible to another. A thread that reads without the lock has no guarantee of ever seeing the new value; the CPU or the JIT compiler may keep using an old copy. Every read and every write of shared, changing data must go through the same lock (or use volatile or an atomic class, below). A synchronized setter with a plain getter is one of the most common findings in code review.
volatile
Marking a field volatile promises that every read sees the latest write, from any thread. It's lighter than a lock, and it's right for two jobs:
- Flags:
volatile boolean running = true;One thread sets it tofalse; the worker'swhile (running)loop is guaranteed to notice. - Publishing a reference: build a new, complete settings object, then assign it to a
volatilefield. Readers see either the old object or the new one, fully built.
What volatile does not do is make count++ safe. The read and the write are still two separate steps, and two threads can still interleave them. For counters, use AtomicLong (or LongAdder), and for updates that touch several fields together, use a lock.
Happens-before
Why do locks and volatile make changes visible? Because of the rule underneath them. Modern CPUs keep values in caches and reorder instructions, and the JIT compiler moves and removes reads to make code faster. So Java promises only this: a thread is guaranteed to see another thread's writes if there's a happens-before connection between them. These create one:
| Connection | Means |
|---|---|
| Lock | releasing a lock happens-before the next time any thread takes the same lock |
volatile | a write happens-before every later read of the same field |
start() | everything before t.start() happens-before everything t does |
join() | everything t does happens-before t.join() returns |
| Handoffs | putting into a BlockingQueue or concurrent map, or submitting to an executor, happens-before the other side takes it |
Connections chain together. If you write some ordinary fields and then set a volatile flag, a thread that sees the flag also sees those fields. When two threads access the same variable, at least one writes, and there's no connection between them, that's a data race, and Java promises very little about what the reader sees.
Immutable objects, and leaking this
An immutable object (all fields final, set in the constructor) is the easiest thing to share between threads: once it's built, no thread can change it, and Java guarantees that every thread sees its final fields fully set.
That guarantee has one condition: the constructor must not let this escape before it finishes.
class Listener {
private final Map<String, Integer> counts;
Listener(EventBus bus) {
bus.register(this); // another thread can call onEvent right now...
counts = new HashMap<>(); // ...before this line has run
}
void onEvent(String e) { counts.merge(e, 1, Integer::sum); } // NullPointerException
}
Registering a listener, starting a thread, or storing this in a static field from inside a constructor hands out a half-built object. Build first, register after; a static factory method such as Listener.create(bus) does both in the right order.
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
count++ from several threads with no lock | lost updates | a lock, or AtomicLong |
| Locking a field you reassign, or a boxed value | threads lock different objects | a private final Object lock |
synchronized (new Object()) | nothing is protected | one shared lock object |
| Static data protected by instance locks | each instance has its own lock | a static lock |
| Synchronized setter, plain getter | readers may never see the change | lock the getter too, or volatile |
volatile counter with ++ | still loses updates | AtomicLong / LongAdder |
A non-volatile stop flag | the loop may never see it | volatile boolean |
this passed out of a constructor | others see a half-built object | a static factory method |
Check yourself
Try to answer before opening each one. Recalling beats re-reading.
1. Why does count++ from two threads lose updates?
Show answer
It's three steps (read, add, write). Both threads can read the same old value and both write the same new one, so one increment disappears.
2. Why doesn't synchronized (count) { count++; } work when count is an Integer field?
Show answer
count++ puts a new Integer object in the field, so threads lock different objects depending on when they arrive, and different locks don't exclude each other. Lock a private final object instead.
3. A setter is synchronized but its getter isn't. What can a reading thread see?
Show answer
An old value, possibly forever: with no lock on the read, there's no happens-before connection to the write. Synchronize the getter on the same lock, or make the field volatile.
4. When is volatile enough, and when isn't it?
Show answer
It's enough for a flag one thread sets and others read, and for publishing a reference to a complete object. It isn't enough for read-modify-write like count++, or for keeping several fields consistent with each other: use an atomic class or a lock.
Key takeaways
count++is three steps; without protection, concurrent threads lose updates.synchronizedexcludes only threads locking the same object: lock aprivate finalfield, for reads as well as writes.volatileis for flags and publishing references, never for++.- Visibility comes from happens-before connections (lock,
volatile,start,join, handoffs), not from timing. - Immutable objects are safe to share, as long as
thisnever escapes the constructor.