☕ Java

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

A race condition occurs when the correctness of a computation depends on the relative timing of operations in two or more threads. The canonical example is a counter incremented by multiple threads: the operation count++ is not atomic — it is a read-modify-write sequence of three separate CPU instructions (load the value, increment it, store it back). If two threads execute this sequence concurrently, both may read the same value, both increment independently, and both store the same result, effectively losing one increment. A counter that should reach 200,000 after two threads each increment 100,000 times may stop anywhere from 100,001 to 200,000, nondeterministically. Atomicity and visibility are two distinct problems that synchronization solves together. Atomicity means that a compound action (read-modify-write, check-then-act, read-read-act) executes as an indivisible unit — no other thread can observe an intermediate state. Visibility means that a write made by one thread is actually seen by other threads. In the Java Memory Model, threads are permitted to cache values in registers or CPU caches; without a visibility guarantee, a write by thread A to a field may remain invisible to thread B indefinitely. The Java Memory Model (JMM) formalizes visibility through happens-before relationships. If action A happens-before action B, then A's memory writes are visible to B. An unlock of monitor M happens-before every subsequent lock of M. A write to a volatile variable happens-before every subsequent read of that same variable. Thread.start() happens-before any action in the started thread. Thread.join() returning happens-before the joined thread's final actions are visible. Without these relationships, there is no guarantee of visibility — the compiler and CPU are free to reorder operations and cache values. The combination of these two problems means that even operations that appear atomic in Java source code may not be. Writing a long or double is not guaranteed to be atomic on 32-bit JVMs (two 32-bit stores may be observed as a partial write). Reading or writing an object reference is atomic on all JVMs (references are word-sized or pointer-sized), but atomicity of the reference write does not guarantee visibility or atomicity of the object's fields.
Java
// ── 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 it

The Monitor Model — Intrinsic Locks and Reentrance

Every Java object has exactly one intrinsic lock, also called a monitor. The monitor is a mutual exclusion mechanism: at most one thread may hold a given object's monitor at any time. When a thread enters a synchronized block or synchronized method on an object, it acquires that object's monitor. When it exits the block or method (either normally or by throwing an exception), it releases the monitor. Any other thread attempting to acquire the same monitor blocks in the BLOCKED state until the monitor becomes available. Static synchronized methods use the Class object's monitor rather than an instance's monitor. Synchronizing on MyClass.class is equivalent to using a static synchronized method — both use the same lock. An instance lock and the class's static lock are completely independent; holding one does not preclude acquiring the other. Java's intrinsic locks are reentrant: a thread that already holds a monitor can acquire it again without blocking. This is essential for synchronized method calls within a synchronized context. If a synchronized method calls another synchronized method on the same object, the thread simply increments a reentrancy counter on the already-held lock rather than blocking on itself. Each exit decrements the counter; the lock is released only when the count reaches zero. Without reentrancy, calling a synchronized method from within another synchronized method on the same object would deadlock — the thread would block waiting for itself to release a lock it cannot release because it is blocked. The monitor model guarantees that the synchronized block's body executes atomically with respect to other threads attempting to synchronize on the same monitor. It also establishes the happens-before relationship: all writes performed before a monitor unlock are visible to any thread that subsequently acquires the same monitor. This is what makes synchronized not just a mutual exclusion mechanism but also a memory visibility mechanism — it is both.
Java
// ── 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 synchronizedclass 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

Deadlock occurs when two or more threads are each waiting for a lock held by another, forming a cycle of dependencies with no thread able to proceed. The classic example: Thread A holds lock X and waits for lock Y; Thread B holds lock Y and waits for lock X. Neither can progress, and both wait forever. Deadlock is silent — no exception is thrown, and the threads simply stop making progress. Detection requires thread dumps or monitoring tools that show threads in BLOCKED state with a cyclic lock dependency. Four conditions must hold simultaneously for deadlock to occur: mutual exclusion (the resources involved are locks that only one thread can hold), hold and wait (threads hold locks while waiting for additional locks), no preemption (locks cannot be forcibly taken from a thread), and circular wait (there is a cycle in the wait-for graph). The standard prevention strategies address the fourth condition: always acquire locks in a consistent global order. If every thread acquires lock X before lock Y, the cycle cannot form. Additional strategies: use tryLock() with a timeout (from java.util.concurrent.locks.Lock), which allows a thread to back off and retry if it cannot acquire all needed locks; use coarser locking so that one lock protects multiple resources, eliminating the multi-lock scenario; or restructure the code to avoid holding a lock while acquiring another. Livelock is a situation where threads are not blocked but are continuously reacting to each other without making progress — like two people in a corridor who both step aside in the same direction repeatedly. Neither is blocked, but neither proceeds. Livelock can result from retry-on-failure logic where both threads repeatedly detect a conflict and retry simultaneously. Starvation occurs when a thread is perpetually denied access to a resource it needs because other threads are always prioritized. In Java's intrinsic lock model, there is no fairness guarantee: a thread that releases and immediately reacquires a lock may do so repeatedly, starving a waiting thread. java.util.concurrent.locks.ReentrantLock with fair=true provides a fairness guarantee at the cost of throughput.
Java
// ── 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

Related Topics in Multithreading

Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Thread Basics
A Java thread is an instance of java.lang.Thread that represents an independent path of execution within the JVM process. Every thread has a lifecycle — from creation through runnable, running, blocked, waiting, timed-waiting, and terminated states — and a set of properties including its name, priority, daemon status, thread group, and uncaught exception handler. The Java memory model specifies what visibility guarantees exist between threads and when writes by one thread are guaranteed to be visible to another. Thread scheduling is controlled by the OS scheduler subject to hints from the JVM via thread priority; the JVM does not provide real-time scheduling guarantees. This entry covers the complete thread lifecycle and its state machine, thread properties and how they affect scheduling and JVM shutdown, the happens-before relationship and why it matters for visibility, daemon threads and their relationship to JVM shutdown, thread interruption as a cooperative cancellation mechanism, and the methods on Thread that every Java developer must understand.
Creating Threads
Java provides three primary abstractions for defining the work a thread will execute: the Thread class itself (subclassed to override run()), the Runnable interface (a task with no return value and no checked exception), and the Callable interface (a task with a return value and a declared checked exception). Each represents a different contract between the task and the infrastructure that runs it. Thread subclassing couples the task definition to the execution mechanism and is the oldest and least flexible approach. Runnable decouples the task from the thread, allowing the same Runnable to be submitted to thread pools, scheduled executors, or wrapped in Thread objects. Callable extends that decoupling to include a return value and exception propagation, returning a Future that allows the caller to retrieve the result or handle exceptions asynchronously. Understanding all three — their contracts, their limitations, and when to use each — is the foundation of concurrent programming in Java before reaching for higher-level constructs.
Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.