Synchronize a Shared Counter in Java
Problem
A race condition happens when threads read and write shared data at the same time, so some updates overwrite others; synchronized lets only one thread run the guarded code at once.
Have two threads each increment a shared counter 10,000 times, and make sure the final value is exactly 20,000.
Java Program
public class SynchronizedCounter {
private int count = 0;
synchronized void increment() { // only one thread at a time may run this
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Runnable work = () -> {
for (int i = 0; i < 10_000; i++) {
counter.increment();
}
};
Thread a = new Thread(work);
Thread b = new Thread(work);
a.start();
b.start();
a.join();
b.join();
System.out.println("Final count: " + counter.count + " (expected 20000)");
}
}Output
Core Logic
count++ is really three steps — read, add, write — so the method that performs it is marked synchronized to make those steps happen as one uninterrupted unit.
- 1
synchronized void increment()locks the counter object for the duration of the call. - 2Both threads run the same
worktask, callingincrement()10,000 times each. - 3A thread that finds the lock held waits until the other thread releases it.
- 4After both
join()calls, main prints the final value.
synchronized, both threads can read 41, both write 42, and one increment is lost — runs typically end below 20,000 by a different amount each time.Key Point: Synchronization fixes correctness but serialises the critical section; keep synchronized blocks as small as possible so threads spend little time waiting.
Key Concepts
Approach 2: Using AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
public static void main(String[] args) throws InterruptedException {
AtomicInteger count = new AtomicInteger(); // thread-safe without any lock
Runnable work = () -> {
for (int i = 0; i < 10_000; i++) {
count.incrementAndGet(); // read, add one and write back as a single atomic step
}
};
Thread a = new Thread(work);
Thread b = new Thread(work);
a.start();
b.start();
a.join();
b.join();
System.out.println("Final count: " + count.get() + " (expected 20000)");
}
}
Output
Core Logic
AtomicInteger performs the read-add-write as a single hardware-supported atomic operation, so no lock is needed at all.
- 1The counter is an
AtomicIntegerstarting at 0. - 2
incrementAndGet()adds one atomically and returns the new value. - 3Both threads hammer the same counter, and
get()reads the final total.
synchronized keyword anywhere.Key Point: For a single counter or flag, atomics are simpler and usually faster; use synchronized or locks when several fields must change together.
Key Concepts
Interview Questions
Does volatile fix the counter race?
No. volatile makes each read and write visible to other threads, but count++ is still a read, an add and a write, and two threads can interleave them.
What does synchronized lock on?
On an instance method, the object (this). On a static method, the Class object. A synchronized (x) block locks whatever object x refers to.