Java Programs›Multithreading›Synchronize a Shared Counter

Synchronize a Shared Counter in Java

intermediate·  Multithreading  ·  Concurrency

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.

Input
2 threads × 10,000 increments
Output
20000

Java Program

Java
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

Final count: 20000 (expected 20000)

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.

How It Works
  1. 1synchronized void increment() locks the counter object for the duration of the call.
  2. 2Both threads run the same work task, calling increment() 10,000 times each.
  3. 3A thread that finds the lock held waits until the other thread releases it.
  4. 4After both join() calls, main prints the final value.
Without 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

synchronizedrace conditionintrinsic lockjoin()

Approach 2: Using AtomicInteger

Java
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

Final count: 20000 (expected 20000)

Core Logic

AtomicInteger performs the read-add-write as a single hardware-supported atomic operation, so no lock is needed at all.

How It Works
  1. 1The counter is an AtomicInteger starting at 0.
  2. 2incrementAndGet() adds one atomically and returns the new value.
  3. 3Both threads hammer the same counter, and get() reads the final total.
All 20,000 increments are counted, without a 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

AtomicIntegerincrementAndGet()lock-free

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.

Learn the Concept

Related Programs