Synchronization
Synchronization in Java is the mechanism by which the JVM enforces mutual exclusion and visibility across threads. Without synchronization, two threads accessing shared mutable state simultaneously produce data races: the program's behavior becomes undefined, computations produce wrong results, and objects are left in inconsistent states. Java synchronization is built on the monitor model: every object has an intrinsic lock (also called a monitor), and threads must acquire that lock before executing a synchronized block or method. Only one thread holds a given monitor at a time; all others block until it is released. Beyond mutual exclusion, synchronization also establishes happens-before relationships that govern memory visibility — ensuring that a thread's writes are seen by subsequent readers. This entry covers the memory model foundation of synchronization, race conditions and what causes them, the monitor and intrinsic lock model, atomicity and visibility as distinct problems, reentrant locking, deadlock conditions and how to avoid them, liveness problems (livelock and starvation), and the relationship between synchronization and the Java Memory Model.
Race Conditions, Atomicity, and Visibility
// ── Race condition: lost increment ────────────────────────────────────
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // NOT atomic: read count, add 1, write count — three operations
}
public int get() { return count; }
}
UnsafeCounter counter = new UnsafeCounter();
Runnable task = () -> {
for (int i = 0; i < 100_000; i++) counter.increment();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter.get()); // Likely NOT 200000 — lost updates due to race
// ── Visibility problem — without synchronization ───────────────────────
public class VisibilityProblem {
private static boolean running = true; // no volatile, no sync
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
// This thread may NEVER see running = false —
// it may cache 'running' in a register indefinitely:
int iterations = 0;
while (running) iterations++;
System.out.println("Stopped after: " + iterations);
});
worker.start();
Thread.sleep(1000);
running = false; // write may not be visible to worker thread
System.out.println("Set running = false");
// worker may loop forever — this is a real JVM behavior
}
}
// ── Fix: volatile for visibility (not sufficient for atomicity) ────────
public class VisibilityFixed {
private static volatile boolean running = true; // volatile: write is immediately visible
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {} // guaranteed to see running = false
System.out.println("Stopped");
});
worker.start();
Thread.sleep(1000);
running = false; // visible to all threads immediately
}
}
// ── Atomicity problem: check-then-act race ────────────────────────────
public class LazyInit {
private static Object instance = null;
// BROKEN — two threads may both see instance == null and both create it:
public static Object getInstance() {
if (instance == null) { // Thread A reads null
// Thread B reads null (before A writes)
instance = new Object(); // Thread A writes
// Thread B writes — double-initialization
}
return instance;
}
}
// ── happens-before relationships ──────────────────────────────────────
// 1. Monitor unlock → subsequent lock of same monitor
// 2. volatile write → subsequent read of same variable
// 3. Thread.start() → any action in the started thread
// 4. Thread.join() returning → the joined thread's last action
int[] sharedData = new int[1];
Thread writer = new Thread(() -> {
sharedData[0] = 42; // action W
});
writer.start();
writer.join(); // join() creates happens-before: W happens-before the next line
System.out.println(sharedData[0]); // guaranteed to see 42 — happens-before ensures itThe Monitor Model — Intrinsic Locks and Reentrance
// ── Intrinsic lock on an object ──────────────────────────────────────
public class SafeCounter {
private int count = 0;
// Acquiring 'this' monitor before executing:
public synchronized void increment() {
count++; // now atomic AND visible: only one thread at a time
}
public synchronized int get() {
return count; // synchronized: reads the correct value (visibility)
}
}
SafeCounter sc = new SafeCounter();
Runnable task = () -> { for (int i = 0; i < 100_000; i++) sc.increment(); };
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(sc.get()); // always 200000 — mutual exclusion prevents races
// ── Static synchronized — Class monitor ──────────────────────────────
public class Registry {
private static int registrationCount = 0;
// Acquires Registry.class monitor — independent of any instance lock:
public static synchronized void register() {
registrationCount++;
}
public static synchronized int getCount() {
return registrationCount;
}
}
// ── Explicit synchronized block — same lock, finer scope ──────────────
public class PartialSync {
private int count = 0;
private final Object lock = new Object(); // dedicated lock object
public void doWork() {
// Non-synchronized section — runs concurrently with other threads:
String data = expensiveComputation(); // no lock held here
// Only this critical section is locked:
synchronized (lock) {
count++; // mutual exclusion for count update only
}
// Non-synchronized section — lock already released:
logResult(data);
}
private String expensiveComputation() { return "result"; }
private void logResult(String s) { System.out.println(s); }
}
// ── Reentrant locking — thread can re-acquire its own lock ──────────
public class ReentrantExample {
public synchronized void outer() {
System.out.println("outer — holding lock");
inner(); // calls another synchronized method on same object
}
public synchronized void inner() {
// Same thread already holds 'this' monitor — reenters without blocking:
System.out.println("inner — lock reacquired (count now 2)");
} // lock count decrements to 1 on exit
} // lock count decrements to 0 on exit from outer — monitor released
new ReentrantExample().outer();
// outer — holding lock
// inner — lock reacquired (count now 2)
// ── Instance lock vs class lock — completely independent ──────────────
public class TwoLocks {
public synchronized void instanceMethod() {
// holds 'this' lock
staticMethod(); // acquiring TwoLocks.class lock — different lock, no deadlock
}
public static synchronized void staticMethod() {
// holds TwoLocks.class lock
System.out.println("static synchronized — class lock");
}
}
// ── Lock release guaranteed even on exception ─────────────────────────
public class SafeRelease {
public synchronized void riskyMethod() {
try {
throw new RuntimeException("error");
} finally {
// Lock is released by the JVM when synchronized block exits,
// whether by normal return, exception, or return statement.
// No explicit release needed — the JVM handles it.
}
}
}Deadlock, Livelock, and Starvation
// ── Classic deadlock ──────────────────────────────────────────────────
public class DeadlockExample {
private final Object lockA = new Object();
private final Object lockB = new Object();
public void methodA() {
synchronized (lockA) { // Thread 1: acquires lockA
System.out.println("A: holding lockA, waiting for lockB");
synchronized (lockB) { // Thread 1: waits for lockB
System.out.println("A: holding both");
}
}
}
public void methodB() {
synchronized (lockB) { // Thread 2: acquires lockB
System.out.println("B: holding lockB, waiting for lockA");
synchronized (lockA) { // Thread 2: waits for lockA → DEADLOCK
System.out.println("B: holding both");
}
}
}
}
DeadlockExample d = new DeadlockExample();
Thread t1 = new Thread(d::methodA);
Thread t2 = new Thread(d::methodB);
t1.start(); t2.start(); // may deadlock — methodA and methodB acquire locks in opposite order
// ── Fix: consistent lock ordering ─────────────────────────────────────
public class DeadlockFixed {
private final Object lockA = new Object();
private final Object lockB = new Object();
// Both methods acquire in the SAME order: lockA first, then lockB
public void methodA() {
synchronized (lockA) {
synchronized (lockB) { // consistent order — deadlock impossible
System.out.println("methodA: holding both");
}
}
}
public void methodB() {
synchronized (lockA) { // same order as methodA
synchronized (lockB) {
System.out.println("methodB: holding both");
}
}
}
}
// ── Fix: tryLock with timeout (ReentrantLock) ─────────────────────────
import java.util.concurrent.locks.*;
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();
boolean transferDeadlockSafe(ReentrantLock from, ReentrantLock to) throws InterruptedException {
while (true) {
if (from.tryLock(50, TimeUnit.MILLISECONDS)) { // try lockA for 50ms
try {
if (to.tryLock(50, TimeUnit.MILLISECONDS)) { // try lockB for 50ms
try {
System.out.println("Both acquired — doing work");
return true;
} finally { to.unlock(); }
}
// Could not get lockB — release lockA and retry
} finally { from.unlock(); }
}
// Neither held — sleep briefly to reduce contention before retry
Thread.sleep(1 + (long)(Math.random() * 10));
}
}
// ── Detecting deadlock at runtime via thread dump ─────────────────────
// Run: kill -3 <pid> (Unix) or Ctrl+Break (Windows) to print thread dump
// JConsole / VisualVM also show deadlock detection automatically
// Thread dump shows (for deadlocked threads):
// "Thread-0" BLOCKED on object@1234 owned by "Thread-1"
// "Thread-1" BLOCKED on object@5678 owned by "Thread-0"
// Found one Java-level deadlock: ← JVM detects and reports the cycle
// ── Starvation with intrinsic locks — no fairness guarantee ──────────
public class StarvationRisk {
private synchronized void hoggedMethod() throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " acquired");
Thread.sleep(10); // holds the lock for 10ms
// On exit, the lock goes to ANY waiting thread — not FIFO
// High-priority or lucky threads can starve low-priority ones
}
}
// Fix: ReentrantLock with fairness:
ReentrantLock fairLock = new ReentrantLock(true); // true = fair mode (FIFO)
// Fair mode guarantees the longest-waiting thread gets the lock next
// Cost: lower throughput due to scheduling overhead