Thread safety: synchronized, volatile & atomic
Race conditions, the monitor lock, visibility, and lock-free counters.
The race condition problem
When two threads read-modify-write the same variable without coordination, the result is unpredictable:
// BROKEN — two threads calling increment() can both read 0, both add 1, both write 1
class Counter {
int count = 0;
void increment() { count++; } // NOT atomic: read + add + write = 3 separate operations
}
synchronized methods and blocks use a monitor lock (every object has one) to ensure at most one thread executes at a time:
class Counter {
int count = 0;
synchronized void increment() { count++; } // method lock on `this`
void decrement() {
synchronized (this) { count--; } // explicit block — same effect
}
}
When a thread enters a synchronized method, other threads calling any synchronized method on the same object must wait.
volatile guarantees visibility: a write to a volatile variable is immediately visible to other threads (no CPU cache hiding). It does not make compound operations like count++ atomic:
volatile boolean running = true; // one thread writes, another reads — safe
java.util.concurrent.atomic provides lock-free atomic types:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // atomic: read + increment + write, uninterruptible
counter.compareAndSet(5, 10); // CAS: sets to 10 only if current value is 5
AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference are the main types. Prefer them over synchronized for simple counters — they are faster and have no blocking.