Command Palette

Search for a command to run...

Lesson 41.1 · Concurrency-Aware Data Structures

Race Conditions, Locks and Atomics

count++ is three steps (read, add, write); two threads can interleave them and lose updates. synchronized makes a block run one thread at a time; atomics do the same for single variables without a lock.

16 min

Think of it like this

Two cashiers updating the same paper ledger: both read "100", both add their sale, both write their own total. One sale disappears. A rule that only one cashier may hold the pen at a time fixes it.

1.What goes wrong

A race condition happens when the result depends on the timing between threads. count++ compiles to load, add and store; if two threads load the same value, one increment is lost. Without synchronisation there's also a visibility problem: one thread may not see another's write for a long time.

synchronized (or a ReentrantLock) gives mutual exclusion and visibility: only one thread runs the block at a time, and changes made inside are visible to the next thread that takes the lock.

Atomics (AtomicInteger, AtomicLong, LongAdder) use CPU compare-and-swap instructions: incrementAndGet() is a single indivisible step. They're faster than a lock for one variable, but can't make several variables change together.

volatile only guarantees visibility, not atomicity: a volatile int still loses updates with ++.

Main.java
import java.util.concurrent.atomic.AtomicInteger;

public class Main {
    static int syncCount = 0;
    static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomic = new AtomicInteger();
        Runnable work = () -> {
            for (int i = 0; i < 100_000; i++) {
                synchronized (lock) { syncCount++; }   // one thread at a time
                atomic.incrementAndGet();              // lock-free, indivisible
            }
        };
        Thread a = new Thread(work), b = new Thread(work);
        a.start(); b.start();
        a.join(); b.join();
        System.out.println("synchronized: " + syncCount);
        System.out.println("atomic: " + atomic.get());
    }
}

Output

synchronized: 200000
atomic: 200000

Remember

  • Read-modify-write needs a lock or an atomic.
  • volatile = visibility only.
  • Keep critical sections short.

Common mistakes

  • Synchronising on different lock objects in different methods.
  • Calling slow I/O while holding a lock.

Words used in this lesson

Thread
An independent path of execution sharing the process's memory.
Race condition
A bug where the result depends on how threads happen to interleave.
Mutual exclusion
Only one thread may be inside a critical section at a time.
Deadlock
Threads waiting for each other's locks forever.