Java · Course navigation

Java: mutual exclusion with synchronized

Protect a shared counter from lost updates when two threads increment it.

On this page
Before you start

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

Java
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);
  }
}
Output
counter=200000

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1class Main {
2 private static int counter = 0;
3 private static final Object lock = new Object();
4
5 static void work() {
6 for (int i = 0; i < 100000; i++) {
7 synchronized (lock) {
8 counter++;
9 }
10 }
11 }
12
13 public static void main(String[] args) throws InterruptedException {
14 Thread a = new Thread(Main::work);
15 Thread b = new Thread(Main::work);
16 a.start();
17 b.start();
18 a.join();
19 b.join();
20 System.out.println("counter=" + counter);
21 }
22}

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
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput 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.

Further reading