Java: mutual exclusion with synchronized
Protect a shared counter from lost updates when two threads increment it.
On this page
Java threads: Thread, start, and join.
- Protect a shared counter from lost updates when two threads increment it.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
A race condition happens when threads read and write shared data without coordination. The statement counter++ is really three steps: read, add one, write. If two threads interleave those steps, one update can overwrite the other and increments are lost.
A critical section is the code that touches the shared data. Wrapping it in synchronized on a shared lock object lets only one thread enter at a time, so every increment completes before the next begins. join waits for a thread to finish, which also guarantees that the main thread sees the final value.
Code example
class Main {
private static int counter = 0;
private static final Object lock = new Object();
static void work() {
for (int i = 0; i < 100000; i++) {
synchronized (lock) {
counter++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(Main::work);
Thread b = new Thread(Main::work);
a.start();
b.start();
a.join();
b.join();
System.out.println("counter=" + counter);
}
}counter=200000
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
class Main { private static int counter = 0; private static final Object lock = new Object(); static void work() { for (int i = 0; i < 100000; i++) { synchronized (lock) { counter++; } } } public static void main(String[] args) throws InterruptedException { Thread a = new Thread(Main::work); Thread b = new Thread(Main::work); a.start(); b.start(); a.join(); b.join(); System.out.println("counter=" + counter); }}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| A thread must hold the lock to enter this block; the other thread waits. | — | — | |
| Read, add, and write happen as one protected unit. | counter increases by 1 per pass, never lost | — | |
| The main thread waits for thread a to finish. | counter increases by 1 per pass, never lost | — | |
| Both threads did 100000 increments, so the total is exact. | counter = 200000 | counter=200000 |
Behind the scenes
How Java processes this code
The Java compiler produces bytecode. The JVM loads the class and invokes its main method.
Cost and common mistakes
O(n) total work; the lock serializes the increments, so two threads give no speed-up here.
- Without synchronized, the result is usually below 200000 and changes from run to run.
- Every access to the shared variable must use the same lock. Locking only the writes is not enough.
- Holding a lock for long periods or taking several locks in different orders can cause contention or deadlock.
Try it yourself
What is a lock-free alternative for this counter?
Reveal answer and reasoning
Use java.util.concurrent.atomic.AtomicInteger and call incrementAndGet(); it makes the update atomic without a synchronized block.