Race at the Counter

Priya’s pipeline handles orders fast. But during the flash sale, something odd shows up on her dashboard. QuickCart sold 100 hoodies. The counter says 97. Two cashiers — two threads — updated the same sold count at the same time. One update got lost. This is a race condition.

Why count++ is not safe

Looks like one line. It is actually three steps:

  1. Read the current value.
  2. Add one.
  3. Write the new value back.

Two threads can interleave those steps and overwrite each other:

Thread 1: Read count = 5
Thread 2: Read count = 5
Thread 1: Increment to 6, Write 6
Thread 2: Increment to 6, Write 6
Result: count = 6  (should be 7!)

Both threads read 5. Both write 6. One sale vanishes from the count.

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // NOT THREAD-SAFE!
    }

    public int getCount() {
        return count;
    }
}

This broken counter is exactly what happened to Priya’s hoodie inventory.

synchronized — one at a time

Synchronized means only one thread can enter the protected code at a time. Others wait their turn.

Synchronized method

public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

The lock belongs to the object (this). While one thread runs increment(), no other thread can run any synchronized method on that same object.

Synchronized block

For finer control, lock only the small part that needs protection:

public class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Keep synchronized blocks small. Do not hold a lock while calling slow APIs or reading files — other threads will wait too long.

Synchronized static method

For static shared data, the lock is on the class object:

public class GlobalOrderCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }
}
// Lock is on GlobalOrderCounter.class

Intrinsic lock

synchronized uses an intrinsic lock (also called a monitor lock) built into every Java object. When you enter a synchronized block, you acquire that lock. When you leave — normally or by exception — the lock is released automatically.

// Intrinsic lock on 'this'
public synchronized void method() { }

// Explicit lock object — often safer for public classes
private final Object lock = new Object();
synchronized (lock) { }

QuickCart sold-count fix

Priya protects her inventory counter:

public class SoldCountTracker {
    private int soldCount = 0;
    private final Object lock = new Object();

    public void recordSale() {
        synchronized (lock) {
            soldCount++;
        }
    }

    public int getSoldCount() {
        synchronized (lock) {
            return soldCount;
        }
    }
}

Now two threads cannot read and write at the same time. Every sale counts.

Thread-safe counter with decrement

public class ThreadSafeCounter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public void decrement() {
        synchronized (lock) {
            count--;
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

volatile — visibility, not safety

There is a second problem besides races: visibility. One thread changes a variable. Another thread might not see the change right away because each CPU keeps its own cached copy.

public class Task {
    private boolean running = true;

    public void stop() {
        running = false; // Other threads might not see this!
    }

    public void run() {
        while (running) {
            // Could run forever
        }
    }
}

volatile tells the JVM: always read and write this variable from main memory, not a stale cache.

public class Task {
    private volatile boolean running = true;

    public void stop() {
        running = false; // Now visible to all threads
    }

    public void run() {
        while (running) {
            // Will see the update
        }
    }
}

What volatile does (plain English)

  1. Visibility — when one thread writes a volatile variable, other threads see the new value quickly.
  2. Happens-before — a write to a volatile variable happens-before any later read of that same variable. In plain English: the reader will not see stale data from before the write.
  3. NOT atomicity — volatile does not make compound operations like count++ safe.

volatile does NOT fix count++

public class Counter {
    private volatile int count = 0;

    public void increment() {
        count++; // STILL NOT THREAD-SAFE!
    }
}

volatile makes each read and write visible. But count++ is still read-then-write — two threads can still interleave. For increment, you need synchronized or an AtomicInteger.

wait() and notify — threads waiting on a lock

When a thread needs to wait for a condition inside synchronized code:

synchronized (lock) {
    while (!orderReady) {
        lock.wait(); // Releases lock, enters WAITING state
    }
    // Process the order...
}

// Another thread, when order is ready:
synchronized (lock) {
    orderReady = true;
    lock.notify(); // Wake one waiting thread
}

wait() must be called inside a synchronized block on the same lock object. It releases the lock so other threads can enter.

Timed wait:

lock.wait(1000); // TIMED_WAITING — wake after 1 second or when notified

When to use synchronized vs volatile

Situation Use
Multiple steps must happen as one unit (read + write) synchronized
Simple flag one thread writes, others read volatile
Compound operations like count++ synchronized or AtomicInteger
Already using thread-safe collections Often nothing extra

synchronized gives you mutual exclusion (one at a time) and visibility.

volatile gives you visibility only — no “one at a time” guarantee.

Use synchronized when

Multiple operations must be atomic:

public synchronized void transfer(Account from, Account to, int amount) {
    from.withdraw(amount);
    to.deposit(amount);
}

Reading and writing shared state together:

public synchronized void update(Map<String, String> data) {
    this.data = new HashMap<>(data);
}

Use volatile when

Single variable, simple read/write:

private volatile boolean shutdown = false;

One thread writes, many threads read — and you only need visibility, not atomicity.

A flag Priya can trust

QuickCart needs a shutdown flag so background workers stop cleanly:

public class QuickCartBackgroundWorker implements Runnable {
    private volatile boolean shutdown = false;

    public void stop() {
        shutdown = true;
    }

    @Override
    public void run() {
        while (!shutdown) {
            processNextEmail();
        }
    }

    private void processNextEmail() {
        // Send pending emails...
    }
}

One thread sets shutdown = true. All workers see it. No synchronized needed — it is a simple write/read of one boolean.

Reentrant locks

Java’s synchronized locks are reentrant. The same thread can enter synchronized code it already holds the lock on:

public synchronized void method1() {
    method2(); // Same thread — allowed
}

public synchronized void method2() {
    // Runs fine — same lock, same thread
}

Without reentrancy, calling method2 from inside method1 would deadlock yourself.

If a synchronized method throws an exception, the lock is still released when the method exits.

Deadlock prevention — lock ordering

Two accounts, two locks — bad ordering can deadlock:

// BAD: Can cause deadlock
public void transfer(Account from, Account to, int amount) {
    synchronized (from) {
        synchronized (to) {
            from.withdraw(amount);
            to.deposit(amount);
        }
    }
}

If Thread 1 locks from then waits for to, while Thread 2 locks to then waits for from, both stall forever.

Fix: always lock in the same order (e.g. by account ID):

// GOOD: Consistent lock ordering
public void transfer(Account from, Account to, int amount) {
    Account first = from.getId() < to.getId() ? from : to;
    Account second = from.getId() < to.getId() ? to : from;

    synchronized (first) {
        synchronized (second) {
            from.withdraw(amount);
            to.deposit(amount);
        }
    }
}

Best practices

Do

  1. Synchronize on private final objects — not on this in public classes.
private final Object lock = new Object();
synchronized (lock) { }
  1. Keep synchronized blocks small — only the shared data update.

  2. Use volatile for simple flags like shutdown or running.

  3. Prefer thread-safe collections when you can:

private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// No extra synchronization needed for basic operations

Do not

  1. Do not synchronize on String literals — they are interned and shared across the JVM.
// BAD
synchronized ("lock") { }

// GOOD
private final Object lock = new Object();
synchronized (lock) { }
  1. Do not use volatile for compound operations like count++.
// BAD
private volatile int count = 0;
count++;

// GOOD
private AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

What to remember

  • Race condition — outcome depends on thread timing; updates get lost.
  • synchronized — one thread at a time; use for read-modify-write like count++.
  • Intrinsic lock — every object has one; synchronized acquires and releases it automatically.
  • volatile — makes changes visible; establishes happens-before; does not make count++ safe.
  • wait/notify — for threads waiting on a condition inside synchronized code.
  • Keep synchronized blocks small.
  • Lock in a consistent order to avoid deadlock.
  • For counters in production, consider AtomicInteger — lock-free and built for this job.

What Priya does next: sometimes she needs finer control — try the lock without waiting forever, or let many readers check stock while only one writer updates it.