Level 2 · 25 min
Concurrency
Java concurrency is essential for building responsive backend systems. Understanding how threads interact, where race conditions arise, and how the JVM memory model guarantees visibility is critical for writing correct multi-threaded code.
Thread Lifecycle and the Happens-Before Relationship
A thread moves through states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. The Java Memory Model (JMM) defines when writes by one thread are visible to another via the happens-before relationship. Without it, the JVM and CPU are free to reorder instructions for performance — leading to visibility bugs that appear only under load.
synchronized, volatile, and Atomic Classes
synchronized provides mutual exclusion (only one thread executes the guarded block at a time) and establishes happens-before between the release of a monitor and its subsequent acquisition. volatile guarantees visibility of writes across threads but NOT atomicity of compound operations — reading then writing a volatile long is two separate operations. AtomicInteger and AtomicLong use compare-and-swap (CAS) CPU instructions to provide atomic compound operations without full mutual exclusion, making them faster under low contention. LongAdder is better than AtomicLong for high-contention counters because it stripes across cells to reduce CAS collisions. The Java Memory Model (JMM) defines a partial ordering called happens-before on all program actions. As specified in the JMM: 'To guarantee that the thread executing action B can see the results of action A (whether or not A and B occur in different threads), there must be a happens-before relationship between A and B. In the absence of a happens-before ordering between two operations, the JVM is free to reorder them as it pleases.' Critically, synchronization serves dual purpose beyond mutual exclusion: Bloch notes in Effective Java that 'many programmers think of synchronization solely as a means of mutual exclusion... This view is correct, but it's only half the story. Without synchronization, one thread's changes may not be visible to other threads.' The volatile keyword guarantees visibility but not atomicity — volatile int nextId = 0; return nextId++ is still broken because nextId++ is a read-modify-write (three operations, not one).
Race Conditions and Deadlocks
A race condition occurs when the correctness of a program depends on the relative timing of threads — the classic check-then-act or read-modify-write performed non-atomically. A deadlock occurs when two or more threads wait for each other to release locks they hold, creating a cycle. Prevention strategies include lock ordering (always acquire locks in the same global order), using tryLock() with a timeout, or preferring higher-level concurrency utilities (ReentrantLock, Semaphore, CountDownLatch) over raw synchronized blocks.
Code example
// UNSAFE: race condition on plain int
private int counter = 0;
public void increment() { counter++; } // not atomic!
// SAFE: AtomicInteger for atomic compound ops
private AtomicInteger counter = new AtomicInteger(0);
public void increment() { counter.incrementAndGet(); }
// SAFE: synchronized for guarding multiple fields
private int x, y;
public synchronized void update(int nx, int ny) {
x = nx;
y = ny;
}
// BETTER for high contention: LongAdder
private LongAdder hits = new LongAdder();
public void recordHit() { hits.increment(); }