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
SafeCounterinstances 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 onthis, 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_mutexon 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:
| Tool | When to use |
|---|---|
ReentrantLock | Need try-lock, timed lock, or fairness policy |
ReadWriteLock | Many reads, few writes — readers don’t block each other |
AtomicInteger / AtomicLong | Single-variable counters without a lock |
ConcurrentHashMap | Thread-safe map with fine-grained segment locking |
BlockingQueue | Producer-consumer handoff without manual wait/notify |
CountDownLatch / CyclicBarrier | Coordinating 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
booleanstop flag shared between threads should bevolatileif you only need visibility, not atomicity. See the volatile keyword page.
Tip: Tools like thread sanitizers, the
jconsoledeadlock detector, and frameworks likeFindBugs/SpotBugscan 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(), andnotifyAll()to coordinate work between threads safely. - Interrupting a Thread — How to signal a thread to stop gracefully using
interrupt()and how threads should handleInterruptedException. - ReentrantLock & Monitors — The
java.util.concurrent.locks.ReentrantLockAPI: timed locks, fairness, conditions, and when it beatssynchronized. - 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.
Related Topics
- 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
synchronizedkeyword. - 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.