Java: mutual exclusion with synchronized

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

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

Explanation

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.

Explore the dry run
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

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