Skip to content
Java synchronization 6 min read

Synchronization

When multiple threads access shared data at the same time, things can go wrong in surprising ways. Java synchronization gives you the tools to coordinate thread access, protect shared state, and keep your program’s behavior predictable — no matter how many threads are running at once.

Why Synchronization Matters

Imagine two threads both trying to increment a counter. Each thread reads the current value, adds one, and writes it back. If both threads read the same value before either writes back, one increment is silently lost. This is called a race condition, and it’s one of the most common bugs in concurrent programs.

public class Counter {
    private int count = 0;

    public void increment() {
        count++; // NOT thread-safe: read-modify-write in three steps
    }

    public int getCount() {
        return count;
    }
}

If two threads call increment() simultaneously, you might expect count to be 2 — but it could end up as 1. The fix is synchronization.

The synchronized Keyword

Java’s built-in answer to race conditions is the synchronized keyword. When you mark a method or block as synchronized, only one thread can execute it at a time. Every Java object has an intrinsic lock (also called a monitor lock). A thread must acquire the lock before entering synchronized code, and releases it when it exits.

public class SafeCounter {
    private int count = 0;

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

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

Now increment() is thread-safe. The JVM ensures that only one thread holds the lock on a SafeCounter instance at any moment.

Note: Synchronization applies per object instance by default. Two threads working on different SafeCounter instances do not block each other.

Synchronized Methods vs. Synchronized Blocks

You can synchronize an entire method or just a critical section of code. Prefer synchronized blocks when only a small part of a method needs protection — they reduce contention and improve throughput.

public class SharedList {
    private final List<String> items = new ArrayList<>();

    // Only the critical section is locked
    public void addItem(String item) {
        // ... some non-critical work here ...
        synchronized (this) {
            items.add(item);
        }
    }
}

A synchronized block takes an explicit lock object (this, a dedicated lock object, or the class). This is more flexible than locking on this for every method.

Tip: Using a private final lock object (e.g., private final Object lock = new Object();) is often better than locking on this, because external code cannot accidentally acquire the same lock.

What the Lock Actually Protects

It’s important to understand: the lock protects a code path, not a variable. If thread A holds the lock and thread B tries to enter any synchronized block on the same object, thread B blocks until thread A releases the lock. But if thread B enters an unsynchronized method that also touches the shared data, there is no protection.

// BAD: read is not synchronized, so the lock offers no protection for readers
public class BrokenCounter {
    private int count = 0;

    public synchronized void increment() { count++; }
    public int getCount() { return count; } // still unsafe!
}

Consistent synchronization on every access — reads and writes — is essential.

Under the Hood: Monitors and the JVM

Every Java object carries a hidden header word in heap memory that stores its monitor state. When a thread executes monitorenter (the bytecode instruction behind synchronized), the JVM checks whether the monitor is free:

  • Biased locking (pre-Java 21): if the monitor has never been contested, the JVM biases it toward the first thread, making subsequent lock/unlock nearly free (just a compare-and-swap on the thread ID in the object header).
  • Thin lock / stack-lock: for briefly contested locks, the JVM uses a lightweight CAS-based protocol without OS involvement.
  • Inflated (fat) lock: if contention is real and ongoing, the JVM escalates to a full OS mutex (pthread_mutex on Linux), which involves a kernel context switch and is significantly more expensive.

Java 21 deprecated biased locking, favouring virtual threads and modern alternatives like java.util.concurrent locks instead.

When a thread holds a monitor and calls wait(), it releases the lock and suspends. When another thread calls notify() or notifyAll(), the waiting thread wakes up and re-acquires the lock before continuing. This is the foundation of inter-thread communication.

Visibility and the Happens-Before Relationship

Synchronization does more than mutual exclusion — it also enforces memory visibility. Without synchronization, the JVM and CPU are free to cache values in registers or reorder instructions. A write on thread A might not be visible to thread B for an indeterminate time.

When thread A exits a synchronized block, all its writes are flushed to main memory. When thread B enters a synchronized block on the same lock, it reads fresh values from main memory. This guarantee is called the happens-before relationship defined by the Java Memory Model.

Warning: Synchronization only provides visibility guarantees between threads that synchronize on the same lock object. Threads using different locks, or no lock at all, get no such guarantee.

java.util.concurrent: Higher-Level Alternatives

The java.util.concurrent package (introduced in Java 5) provides richer tools that often outperform raw synchronized blocks:

ToolWhen to use
ReentrantLockNeed try-lock, timed lock, or fairness policy
ReadWriteLockMany reads, few writes — readers don’t block each other
AtomicInteger / AtomicLongSingle-variable counters without a lock
ConcurrentHashMapThread-safe map with fine-grained segment locking
BlockingQueueProducer-consumer handoff without manual wait/notify
CountDownLatch / CyclicBarrierCoordinating a fixed number of threads

For most new code you should reach for these utilities before writing raw synchronized blocks. They’re well-tested, better documented, and often more performant under contention.

Common Pitfalls

  • Locking on the wrong object. Synchronizing on a local variable or a non-shared object provides no protection.
  • Holding locks too long. Large synchronized blocks serialize too much work and kill throughput.
  • Nested locks in inconsistent order. This is the classic recipe for deadlock.
  • Forgetting volatile for simple flags. A boolean stop flag shared between threads should be volatile if you only need visibility, not atomicity. See the volatile keyword page.

Tip: Tools like thread sanitizers, the jconsole deadlock detector, and frameworks like FindBugs/SpotBugs can catch synchronization bugs that are almost invisible during normal testing.

In This Section

This section covers every aspect of Java synchronization and thread coordination:

  • Synchronized Block — How to lock just a critical section of code instead of an entire method for finer-grained control.
  • Static Synchronization — Locking on the class object rather than an instance, so all instances share a single lock.
  • Deadlock — What deadlock is, how it occurs when threads wait on each other’s locks, and proven strategies to prevent it.
  • Inter-Thread Communication — Using wait(), notify(), and notifyAll() to coordinate work between threads safely.
  • Interrupting a Thread — How to signal a thread to stop gracefully using interrupt() and how threads should handle InterruptedException.
  • ReentrantLock & Monitors — The java.util.concurrent.locks.ReentrantLock API: timed locks, fairness, conditions, and when it beats synchronized.
  • volatile Keyword — Guaranteeing memory visibility for a single shared variable without acquiring a full lock.
  • Java Memory Model — The formal rules the JVM follows for instruction reordering, happens-before, and what is truly safe to assume across threads.
  • Multithreading — The foundation: what threads are and how to create and manage them in Java.
  • Thread Life Cycle — Understanding the states a thread passes through, including BLOCKED and WAITING states caused by synchronization.
  • Deadlock — A deep dive into the most dangerous consequence of incorrect lock ordering.
  • ReentrantLock & Monitors — The modern, flexible alternative to the synchronized keyword.
  • Java Memory Model — The formal spec that defines what synchronized actually guarantees at the JVM level.
  • Concurrent Collections — Ready-made thread-safe data structures so you don’t have to synchronize collection access yourself.
Last updated June 13, 2026
Was this helpful?