☕ Java

synchronized Keyword

The synchronized keyword is Java's built-in mechanism for declaring that a block of code or an entire method must execute under a monitor lock. It can be applied to instance methods (locking on 'this'), static methods (locking on the Class object), or explicit blocks (locking on any object reference). Every synchronized construct has exactly one associated lock object, and the JVM guarantees that at most one thread holds that lock at a time. When a thread enters a synchronized context, it acquires the lock, executes the body, releases the lock on exit (always, even via exception), and establishes a happens-before relationship that makes its writes visible to any thread that subsequently acquires the same lock. This entry covers all four syntactic forms, the scope and granularity implications of each, how synchronized interacts with inheritance and overriding, the common mistake of synchronizing on non-final fields and new objects, lock identity and why object references matter, performance implications of broad vs narrow synchronization, and the relationship between synchronized and volatile.

The Four Forms of synchronized

The synchronized keyword appears in four contexts. A synchronized instance method declares the method with the synchronized modifier: public synchronized void increment(). The lock is always the instance — this — and is acquired on method entry and released on method exit. The entire method body is protected. A synchronized static method uses the synchronized modifier on a static method: public static synchronized void register(). The lock is the Class object — MyClass.class — not any instance. Instance locks and class locks are always independent, so a synchronized instance method and a synchronized static method on the same class do not mutually exclude each other. A synchronized block on this uses an explicit synchronized statement with this as the argument: synchronized (this) { ... }. This is functionally equivalent to making the method synchronized, but allows protecting only a portion of the method body. Narrowing the scope of locking reduces contention and improves throughput when part of the method's work does not need mutual exclusion. A synchronized block on an explicit lock object uses any non-null object reference: synchronized (lock) { ... } where lock is a dedicated field. Explicit lock objects allow finer-grained control: a class can have multiple independent lock objects protecting different groups of fields, so threads doing unrelated operations do not unnecessarily block each other. The lock object must be chosen carefully — it must be the same object across all code paths that protect the same shared state. All four forms share the same guarantee: only one thread executes the synchronized code for a given lock object at a time, and the happens-before relationship is established on lock acquisition and release. The choice between them is a matter of granularity, scope, and which object's monitor semantics are needed.
Java
// ── Form 1: synchronized instance method — lock is 'this' ───────────
public class BankAccount {
    private double balance;

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public synchronized void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        balance += amount;  // protected by 'this' monitor
    }

    public synchronized void withdraw(double amount) {
        if (amount > balance) throw new IllegalStateException("Insufficient funds");
        balance -= amount;  // protected by 'this' monitor
    }

    public synchronized double getBalance() {
        return balance;     // synchronized read — visibility guaranteed
    }
}

BankAccount account = new BankAccount(1000.0);
// Multiple threads can safely call deposit/withdraw — 'this' lock serializes them

// ── Form 2: synchronized static method — lock is the Class object ─────
public class IdGenerator {
    private static long nextId = 0;

    // Acquires IdGenerator.class monitor:
    public static synchronized long next() {
        return ++nextId;
    }

    // Instance method — independent lock, does NOT block static method:
    public synchronized String format(long id) {
        return String.format("ID-%06d", id);
    }
}

long id1 = IdGenerator.next();  // safe from multiple threads — class lock
long id2 = IdGenerator.next();
System.out.println(id1 + " " + id2);  // 1 2

// ── Form 3: synchronized block on this — narrowed scope ───────────────
public class LoggingCounter {
    private int count = 0;
    private final StringBuilder log = new StringBuilder();

    public void increment(String message) {
        // Expensive logging — no need to hold lock during this:
        String entry = "[" + System.nanoTime() + "] " + message;

        // Only the count update needs mutual exclusion:
        synchronized (this) {
            count++;
            log.append(entry).append('
');
        }

        // Post-processing — lock already released:
        System.out.println("Logged: " + message);
    }
}

// ── Form 4: synchronized block on explicit lock object ────────────────
public class TwoGroupCounter {
    private int countA = 0;
    private int countB = 0;

    // Two independent locks — threads in group A and group B don't block each other:
    private final Object lockA = new Object();
    private final Object lockB = new Object();

    public void incrementA() {
        synchronized (lockA) {
            countA++;   // lockA protects countA only
        }
    }

    public void incrementB() {
        synchronized (lockB) {
            countB++;   // lockB protects countB — independent of lockA
        }
    }

    // Needs BOTH locks to read consistently:
    public int total() {
        synchronized (lockA) {
            synchronized (lockB) {
                return countA + countB;   // consistent read of both
            }
        }
    }
}

TwoGroupCounter tgc = new TwoGroupCounter();
// Thread 1 calling incrementA() and Thread 2 calling incrementB() proceed in parallel
// They only contend when total() is called (acquires both locks)

Lock Identity, Common Mistakes, and synchronized with Inheritance

The lock in a synchronized statement is determined by the object reference, not by the variable name or the field. Every synchronized(expr) acquires the monitor of the object that expr currently refers to. This means synchronizing on the wrong object is one of the most common and hardest-to-find concurrency bugs. The most dangerous mistake: synchronizing on a non-final field. If two threads each read the field to use as the lock, and the field is reassigned between their reads, they acquire different monitors and do not mutually exclude each other. A non-final field that serves as a lock creates a race condition in the locking mechanism itself. Lock fields must always be final. Synchronizing on a new object created inline — synchronized (new Object()) — provides no mutual exclusion at all. Each thread creates its own new object, acquires its own unique monitor, and proceeds concurrently. This compiles and runs without error but provides zero protection. Synchronizing on String literals is problematic because string literal interning means that two unrelated classes that both synchronize on the literal "lock" share the same String object (string pool), causing unexpected interference between completely unrelated code. Synchronizing on Integer or other boxed values created via autoboxing is similarly dangerous: Integer values from -128 to 127 are cached and may be shared, while values outside this range are new objects. Always use private final Object instances as dedicated lock objects. The synchronized keyword is not inherited by overriding. If a superclass declares a synchronized method and a subclass overrides it without the synchronized keyword, the override is not synchronized. Each class's method has whatever synchronization it explicitly declares. When calling super.method() from a synchronized override, the super call does not separately acquire the superclass's (non-existent, separate) lock — the lock is still this, the instance — so reentrance handles the super call correctly, but only because synchronized is on this for both calls in the instance method case.
Java
// ── Lock identity — the object reference matters ─────────────────────
public class WrongLock {
    private Object lock = new Object();  // NON-FINAL — dangerous

    public void methodA() {
        synchronized (lock) {            // Thread A reads lock → Object@1234
            lock = new Object();         // lock reassigned mid-execution!
            doWork();
        }
    }

    public void methodB() {
        synchronized (lock) {            // Thread B might read Object@5678 (new object)
            doWork();                    // no mutual exclusion with methodA!
        }
    }
}

// CORRECT — final guarantees same object always:
public class CorrectLock {
    private final Object lock = new Object();  // FINAL — always same object

    public void methodA() {
        synchronized (lock) { doWork(); }  // always Object@1234
    }

    public void methodB() {
        synchronized (lock) { doWork(); }  // always Object@1234 — mutual exclusion ✓
    }
}

// ── Synchronizing on 'new Object()' — zero protection ─────────────────
public void brokenSync() {
    synchronized (new Object()) {     // Each thread creates its own object
        count++;                      // No mutual exclusion — every thread gets its own lock
    }
}

// ── String literal interning hazard ──────────────────────────────────
public class LibraryA {
    public void process() {
        synchronized ("LOCK") {      // uses the interned String "LOCK" from pool
            doWork();
        }
    }
}

public class LibraryB {
    public void process() {
        synchronized ("LOCK") {      // SAME interned String — shares monitor with LibraryA!
            doOtherWork();           // LibraryA and LibraryB unexpectedly block each other
        }
    }
}

// CORRECT — use private final Object:
public class LibraryC {
    private static final Object LOCK = new Object();  // unique object, not interned
    public void process() {
        synchronized (LOCK) { doWork(); }  // no interference with other classes
    }
}

// ── Integer autoboxing — cache range hazard ───────────────────────────
Integer a = 127;                      // cached — always same object
Integer b = 127;
System.out.println(a == b);           // true  — same cached object

Integer c = 128;                      // NOT cached — new object
Integer d = 128;
System.out.println(c == d);           // false — different objects!

// synchronized on Integer 127: unintentionally shared across all code using 127
// synchronized on Integer 128: different threads get different objects — no exclusion

// ── synchronized not inherited by overriding ─────────────────────────
public class Parent {
    public synchronized void criticalMethod() {
        System.out.println("Parent: synchronized");
    }
}

public class Child extends Parent {
    @Override
    public void criticalMethod() {          // NOT synchronized — explicit override
        System.out.println("Child: NOT synchronized");
        super.criticalMethod();             // super call IS synchronized (on 'this')
    }
}

// Child.criticalMethod() can be called by multiple threads concurrently
// The super.criticalMethod() call is protected, but Child's own code before it is not

// ── Correct pattern: always re-declare synchronized in override if needed ──
public class ChildCorrect extends Parent {
    @Override
    public synchronized void criticalMethod() {   // explicit synchronized
        System.out.println("ChildCorrect: synchronized");
        super.criticalMethod();   // reentrant — same lock 'this'
    }
}

Scope, Granularity, Performance, and synchronized vs volatile

The scope of a synchronized block determines how much work is done while holding the lock. Broad scope (synchronized entire method) is simpler to reason about and less error-prone. Narrow scope (synchronized block covering only the minimum critical section) reduces the time the lock is held and thus reduces contention among threads. Reduced contention means less time threads spend blocked waiting for the lock, which translates directly to throughput. The performance cost of synchronization has two components: acquiring and releasing the lock (even an uncontested lock requires a memory barrier, which flushes the thread's cache and synchronizes with main memory — this is a significant but not catastrophic cost), and contention (when threads block waiting for a lock, the blocking and waking process involves OS scheduling, which is expensive). An uncontested synchronized block is orders of magnitude cheaper than a contented one. Optimizing for low contention is therefore the primary performance lever for synchronized code: keep critical sections short, do not perform I/O or blocking operations while holding a lock, and use separate lock objects for independent groups of fields. The volatile keyword solves the visibility problem without solving the atomicity problem. A volatile field write is immediately visible to all threads (happens-before guaranteed), but compound operations (read-modify-write) on volatile fields are not atomic. volatile count++ is still three non-atomic operations. volatile is appropriate for flags (boolean running = true that one thread sets to false and others read), for publishing immutable objects (the reference is written once and read many times), and for the double-checked locking pattern with immutable-object references. volatile is not appropriate for counters, balances, or any state that involves read-modify-write operations. synchronized provides both visibility and atomicity. If you need only visibility and the write is a single field assignment, volatile suffices and is cheaper. If you need atomicity for compound operations, synchronized (or the java.util.concurrent.atomic classes) is required. Using synchronized where volatile suffices wastes throughput; using volatile where synchronized is needed introduces subtle, intermittent bugs.
Java
// ── Granularity: broad vs narrow scope ───────────────────────────────
public class OrderProcessor {
    private final List<String> orders = new ArrayList<>();
    private int processedCount = 0;

    // BROAD: holds lock for entire method including I/O — poor throughput:
    public synchronized void processOrderBroad(String orderId) {
        String data = fetchFromDatabase(orderId);   // I/O while holding lock!
        orders.add(data);
        processedCount++;
    }

    // NARROW: only the state updates are locked — I/O runs concurrently:
    public void processOrderNarrow(String orderId) {
        String data = fetchFromDatabase(orderId);   // I/O without lock — parallel

        synchronized (this) {         // lock held only for state update
            orders.add(data);
            processedCount++;
        }   // lock released immediately — other threads can proceed
    }

    private String fetchFromDatabase(String id) {
        try { Thread.sleep(10); } catch (InterruptedException e) {}  // simulated I/O
        return "Order:" + id;
    }
}

// ── volatile for visibility only — flags and single-write fields ───────
public class TaskRunner {
    private volatile boolean cancelled = false;  // volatile: no sync overhead

    public void run() {
        while (!cancelled) {       // always reads fresh value from main memory
            doUnitOfWork();
        }
        System.out.println("Task cancelled");
    }

    public void cancel() {
        cancelled = true;          // immediately visible to run() — no sync needed
    }
}

// ── volatile does NOT fix compound operations ─────────────────────────
public class BrokenVolatileCounter {
    private volatile int count = 0;

    public void increment() {
        count++;   // STILL NOT ATOMIC — read(count), add 1, write(count) are separate
        // volatile only guarantees each individual read/write is visible
        // It does NOT make the compound read-modify-write sequence atomic
    }
}

// ── synchronized fixes both atomicity AND visibility ──────────────────
public class CorrectCounter {
    private int count = 0;   // does NOT need to be volatile — sync handles visibility

    public synchronized void increment() { count++; }     // atomic + visible
    public synchronized int get()        { return count; } // reads consistent value
}

// ── AtomicInteger: atomic compound operations without synchronized ─────
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment()  { count.incrementAndGet(); }  // lock-free, atomic
    public int  get()        { return count.get(); }
    public int  getAndReset(){ return count.getAndSet(0); } // atomic swap
}

// ── Double-checked locking — requires volatile ────────────────────────
public class Singleton {
    // volatile needed: without it, the reference write might be seen
    // before the constructor completes (due to instruction reordering):
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {                   // first check — no lock (fast path)
            synchronized (Singleton.class) {
                if (instance == null) {           // second check — under lock
                    instance = new Singleton();   // volatile write — constructor completes before reference is visible
                }
            }
        }
        return instance;  // volatile read — always sees fully constructed object
    }
}

// ── Performance comparison: synchronized vs volatile vs atomic ────────
// Uncontested synchronized:  ~20-40ns per operation (memory barrier cost)
// Volatile read/write:       ~5-15ns per operation (memory barrier, no lock)
// AtomicInteger CAS:         ~10-25ns per operation (hardware CAS, no OS involvement)
// Contended synchronized:    ~200-1000ns+ (OS thread scheduling dominates)
// Rule: prefer volatile or Atomic for single-variable operations;
//        use synchronized for compound multi-variable state.

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.