Skip to content
Java synchronization 7 min read

Synchronized Block

When multiple threads share data, things can go wrong fast — values get overwritten, counters drift, and bugs appear only occasionally, making them a nightmare to reproduce. Java’s synchronized keyword fixes this, and the synchronized block gives you a precise, surgical way to protect only the code that truly needs it.

The Problem: Race Conditions

A race condition happens when two or more threads read and write shared data at the same time, and the final result depends on who gets there first. Consider a simple counter:

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // looks atomic — but it is NOT
    }

    public int getCount() { return count; }
}

The line count++ is actually three operations at the bytecode level: read → increment → write. Two threads can interleave these steps and lose one of the increments. After 1 000 threads each call increment() once, you might see 987 instead of 1 000.

synchronized Methods vs synchronized Blocks

Java offers two syntactic forms of synchronization:

Featuresynchronized methodsynchronized block
Lock scopeEntire method bodyOnly the code inside {}
Lock objectthis (instance) or Class (static)Any object you choose
GranularityCoarseFine-grained
PerformanceLower (holds lock longer)Higher (shorter lock window)
ReadabilitySimplerSlightly more verbose

A synchronized block lets you shrink the critical section — only the lines that actually touch shared state need to hold the lock.

Syntax

synchronized (lockObject) {
    // critical section — only one thread at a time runs this
}

lockObject can be any non-null object reference. Common choices are:

  • this — lock on the current instance
  • A dedicated private final Object field — recommended for maximum safety
  • A Class literal like Counter.class — for static shared state

Basic Example

public class Counter {
    private int count = 0;
    private final Object lock = new Object(); // dedicated lock object

    public void increment() {
        // non-critical work can happen here without holding the lock
        synchronized (lock) {
            count++; // critical section
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}

Now only one thread can execute either increment() or getCount() at a time, and the lock is only held for the one line that modifies shared state.

Why Use a Block Instead of a Method?

Imagine a method that does three things: validates input (safe), updates a shared list (unsafe), and logs a message (safe). With a synchronized method the lock is held for all three steps. With a synchronized block you hold the lock only during the list update.

public class OrderProcessor {
    private final List<String> orders = new ArrayList<>();
    private final Object listLock = new Object();

    public void process(String order) {
        // Step 1: validation — no shared state, no lock needed
        if (order == null || order.isBlank()) return;

        // Step 2: update shared list — lock only here
        synchronized (listLock) {
            orders.add(order);
        }

        // Step 3: logging — no shared state, no lock needed
        System.out.println("Processed: " + order);
    }
}

This pattern cuts down on lock contention — the amount of time threads spend waiting for each other.

Tip: Always keep synchronized blocks as short as possible. Every millisecond you hold a lock is a millisecond another thread spends blocked.

Locking on this

Using this as the lock object is equivalent to a synchronized method on the same scope:

public void increment() {
    synchronized (this) {
        count++;
    }
}

This is fine for simple cases, but it exposes the lock to outside code — any other class that holds a reference to your object can also synchronize on this, potentially causing unexpected contention or deadlocks.

Warning: Avoid synchronizing on publicly accessible objects (this, a public field, or a string literal). A private final Object lock = new Object() field is safer because only your class can use it.

Locking on a Class Object (Static Shared State)

When you have static fields shared across all instances, you need a class-level lock:

public class IdGenerator {
    private static int nextId = 0;

    public static int generate() {
        synchronized (IdGenerator.class) {
            return ++nextId;
        }
    }
}

Note: A synchronized instance block using this does NOT protect static fields — each instance has its own this, but static fields belong to the class. Always use ClassName.class or a static lock object for static state.

Real-World Example: Thread-Safe Bank Account

public class BankAccount {
    private double balance;
    private final Object balanceLock = new Object();

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

    public void deposit(double amount) {
        synchronized (balanceLock) {
            balance += amount;
        }
    }

    public boolean withdraw(double amount) {
        synchronized (balanceLock) {
            if (balance >= amount) {
                balance -= amount;
                return true;
            }
            return false;
        }
    }

    public double getBalance() {
        synchronized (balanceLock) {
            return balance;
        }
    }
}

Both deposit and withdraw lock on the same balanceLock, so they can never run at the same time — your balance is always consistent.

Avoiding Deadlock

A deadlock happens when Thread A holds lock X and waits for lock Y, while Thread B holds lock Y and waits for lock X — both are stuck forever.

The most reliable prevention rule: always acquire multiple locks in the same order across all threads.

// Thread 1 and Thread 2 both lock account1 first, then account2
// — consistent ordering prevents deadlock
public static void transfer(BankAccount from, BankAccount to, double amount) {
    BankAccount first  = from.id < to.id ? from : to;
    BankAccount second = from.id < to.id ? to   : from;

    synchronized (first.balanceLock) {
        synchronized (second.balanceLock) {
            if (from.balance >= amount) {
                from.balance -= amount;
                to.balance   += amount;
            }
        }
    }
}

Warning: Nested synchronized blocks are a deadlock risk if the locking order is inconsistent. Consider using java.util.concurrent.locks.ReentrantLock with a timeout (ReentrantLock & Monitors) for safer multi-lock scenarios.

Under the Hood

Every Java object carries a hidden monitor (also called an intrinsic lock). The monitor is a mutual-exclusion device built into the JVM. When a thread enters a synchronized block, it calls the JVM’s monitorenter instruction. When it exits (normally or via exception), it calls monitorexit.

You can verify this yourself with the javap -c disassembler (see javap Tool):

// Simplified bytecode for a synchronized(lock) { count++; } block
monitorenter          // acquire the lock
iinc 2 1             // count++
monitorexit           // release the lock (normal path)
// plus a second monitorexit in the exception handler

Key internal details:

  • Reentrant — a thread that already holds a lock can re-enter synchronized blocks on the same object without deadlocking. The JVM keeps a count of how many times the lock was acquired and only releases it when the count hits zero.
  • Memory visibility — entering a synchronized block flushes the thread’s local CPU cache to main memory; exiting flushes writes back. This guarantees you always see the most recent values. This is the same guarantee provided by the volatile keyword, but synchronization also provides atomicity.
  • Lock biasing (historical) — Java 6–14 used a “biased locking” optimization: if a lock is consistently acquired by only one thread, the JVM elided the expensive atomic CAS operation. Java 15 deprecated this and Java 21 removes it, because modern hardware makes unbiased locking cheap enough.
  • Lock inflation — the JVM starts with a cheap “thin lock” (a CAS on the object header) and inflates to a heavier OS mutex only when contention is detected.

For production concurrent code, also consider java.util.concurrent building blocks (Concurrent Collections, ReentrantLock, AtomicInteger) which often outperform manual synchronization.

Quick Checklist

  • Lock the same object in every method that touches shared state.
  • Keep synchronized blocks as short as possible — only the critical section.
  • Never lock on String literals or boxed Integer values — they may be shared JVM-wide via caching.
  • Read operations on shared state also need synchronization, not just writes.
  • Prefer a private final Object lock field over this to prevent outside interference.
  • Synchronization — the big picture of thread safety and why you need it in the first place
  • Deadlock — understand and avoid the classic multi-lock trap
  • ReentrantLock & Monitors — a more flexible alternative to synchronized blocks with try-lock and timeout support
  • volatile Keyword — lightweight visibility guarantee without mutual exclusion
  • Java Memory Model — the formal rules that govern how threads see each other’s writes
  • Static Synchronization — protecting static shared state across all instances
Last updated June 13, 2026
Was this helpful?