How the Java synchronized Keyword Protects Shared State
java synchronized keyword: Understand how the Java synchronized keyword provides mutual exclusion and visibility guarantees, and when to use methods, blocks, or Reentr...
The java synchronized keyword marks a block or method as a critical section that requires exclusive access. When a thread enters a synchronized block or method, it must acquire the intrinsic lock associated with the object or class. If another thread already holds that lock, the arriving thread blocks until the lock is released. This guarantees that two threads cannot execute the same critical section concurrently, which is the basic mutual exclusion requirement for protecting shared mutable state.
What the synchronized Keyword Actually Does
public class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
The increment and getCount methods both synchronize on the Counter instance. A thread calling increment acquires the lock on that instance, and a thread calling getCount must wait for the same lock. This prevents a reader from observing a partially updated count value.
How the Intrinsic Lock Works
Every Java object has an associated monitor lock, also called an intrinsic lock or monitor. The synchronized keyword uses this lock implicitly. There is no explicit lock object to create or pass; the lock is part of the object itself.
The lock is reentrant. If a thread already holds the lock on an object, it can re-enter another synchronized block or method that synchronizes on the same object. This matters when a synchronized method calls another synchronized method on the same instance.
public class Service { public synchronized void outer() { inner(); // allowed: same thread already holds the lock } public synchronized void inner() { // ... } }
Without reentrancy, outer would deadlock on itself. Reentrancy is tracked per-thread, so the same thread can acquire the lock multiple times and must release it the same number of times before another thread can acquire it.
Synchronized Methods vs Synchronized Blocks
A synchronized method locks the entire method body. The lock is the instance for instance methods, or the Class object for static methods. This is simple but often too coarse. If a method does substantial work that does not touch shared state, holding the lock for the whole method increases contention.
A synchronized block lets you narrow the critical section to only the statements that actually access shared data.
public class Account { private final Object lock = new Object(); private double balance; public void deposit(double amount) { // non-shared work can happen here without the lock double fee = computeFee(amount); synchronized (lock) { balance += amount - fee; } } }
The block also lets you choose an explicit lock object. Using a dedicated lock object instead of this is useful when the class exposes its instance publicly and you do not want external code to interfere with your locking by synchronizing on the same instance.
Static Synchronized Methods and Class-Level Locks
A synchronized static method locks the Class object associated with the class, not any particular instance. This means all instances of the class share the same lock for that static method.
public class Config { private static String value; public static synchronized void setValue(String v) { value = v; } public static synchronized String getValue() { return value; } }
If you have both a synchronized instance method and a synchronized static method in the same class, they use different locks. The instance method locks the instance; the static method locks the Class object. Code that calls both from different threads can run concurrently, which is correct only if they do not access the same shared state.
Visibility Guarantees Beyond Mutual Exclusion
The synchronized keyword provides more than mutual exclusion. It also establishes a happens-before relationship. When a thread exits a synchronized block, any write it performed is visible to a thread that subsequently acquires the same lock. This is what makes the pattern safe for both writes and reads.
public class SharedState { private boolean ready = false; public synchronized void publish() { ready = true; } public synchronized boolean isReady() { return ready; } }
Without the synchronized keyword on isReady, a thread could read a stale ready value even if publish already ran, because there is no happens-before edge. The lock acquisition on the read side establishes that edge.
Performance and Contention
The cost of synchronized depends heavily on contention. When no other thread holds the lock, acquiring it is relatively cheap because the JVM applies biased locking and lightweight locking optimizations. The real cost appears under contention: threads block and are later woken, which involves OS-level thread scheduling.
There is no reliable way to predict the exact overhead without measuring your specific workload. The practical guidance is to keep critical sections short, avoid holding locks during I/O or long computations, and avoid nested locks that can create deadlock.
Common Failure Modes
Deadlock is the most common failure mode with synchronized. It occurs when two threads hold locks and each waits for the other's lock. There is no timeout or interruption mechanism with synchronized; once a thread blocks waiting for a lock, it waits indefinitely.
public class DeadlockExample { private final Object a = new Object(); private final Object b = new Object(); public void one() { synchronized (a) { synchronized (b) { // ... } } } public void two() { synchronized (b) { synchronized (a) { // ... } } } }
If thread T1 runs one and thread T2 runs two, T1 holds a and waits for b, while T2 holds b and waits for a. Neither can proceed. Acquiring locks in a consistent global order across all code paths avoids this.
Another limitation is that synchronized cannot be interrupted. A thread blocked on a synchronized block does not respond to Thread.interrupt() until it acquires the lock. If you need interruptible locking, try-lock with timeout, or fair locking, ReentrantLock from java.util.concurrent.locks is the appropriate alternative.
When to Choose ReentrantLock Instead
ReentrantLock provides the same mutual exclusion semantics but adds features that synchronized does not have: tryLock() with a timeout, lockInterruptibly(), and a constructor flag for fair ordering. It also requires explicit unlock() calls, typically in a finally block, which makes it easier to introduce bugs if you forget to release the lock.
import java.util.concurrent.locks.ReentrantLock; public class Resource { private final ReentrantLock lock = new ReentrantLock(); public void update() { lock.lock(); try { // critical section } finally { lock.unlock(); } } }
n
Use synchronized when the critical section is short, you do not need interruption or timeout, and you prefer the simpler syntax. Use ReentrantLock when you need tryLock, interruptible acquisition, or fair ordering. The performance difference between the two is is small in most modern JVMs; the decision should be driven by required semantics, not micro-benchmarks.