Understanding Java's synchronized Keyword: Lock Types, Mechanics, and JVM Optimizations
Synchronized Lock Semantics
The synchronized keyword in Java enforces mutual exclusion by associating a monitor (intrinsic lock) with an object or class. Its behavior depends on what is being locked:
- Instance-level locks: Applied to non-static methods or
synchronized(this)blocks — each object instance has its own independent lock. - Class-level locks: Applied to
static synchronizedmethods orsynchronized(ClassName.class)blocks — all instances share the same lock tied to theClassobject.
Crucially, acquiring a synchronized block or method automatically releases the lock upon normal completion or uncaught exception — no manual cleanup is required.
Instance Locks: Explicit and Implicit Forms
Synchronized Code Blocks with Custom Lock Objects
Locking on arbitrary objects enables fine-grained control over concurrency boundaries:
public class FineGrainedLocking implements Runnable {
private final Object lockA = new Object();
private final Object lockB = new Object();
@Override
public void run() {
// First critical section — uses lockA
synchronized (lockA) {
System.out.println("Thread " + Thread.currentThread().getName() + " entered section A");
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Thread " + Thread.currentThread().getName() + " exited section A");
}
// Second critical section — uses lockB; runs immediately after lockA is released
synchronized (lockB) {
System.out.println("Thread " + Thread.currentThread().getName() + " entered section B");
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Thread " + Thread.currentThread().getName() + " exited section B");
}
}
public static void main(String[] args) {
FineGrainedLocking task = new FineGrainedLocking();
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
}
}
Because lockA and lockB are distinct objects, threads may interleave execution across sections — one thread can release lockA and immediately acquire lockB, while another concurrently acquires lockA.
Synchronized Instance Methods
When applied to non-static methods, synchronized implicitly locks on this:
public class InstanceMethodLock implements Runnable {
private static final InstanceMethodLock shared = new InstanceMethodLock();
@Override
public void run() {
performCriticalTask();
}
private synchronized void performCriticalTask() {
System.out.println("Thread " + Thread.currentThread().getName() + " acquired instance lock");
try { Thread.sleep(2500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Thread " + Thread.currentThread().getName() + " released instance lock");
}
public static void main(String[] args) {
new Thread(shared, "T-1").start();
new Thread(shared, "T-2").start();
}
}
Here, both threads compete for the same shared instance’s monitor — execution becomes strictly sequential.
Class-Level Locks
These ensure exclusive access across all instances of a class:
Static Synchronized Methods
public class ClassLevelLock implements Runnable {
private static final ClassLevelLock instance1 = new ClassLevelLock();
private static final ClassLevelLock instance2 = new ClassLevelLock();
@Override
public void run() {
executeClassScopedOperation();
}
private static synchronized void executeClassScopedOperation() {
System.out.println("Thread " + Thread.currentThread().getName() + " entered static synchronized method");
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Thread " + Thread.currentThread().getName() + " exited static synchronized method");
}
public static void main(String[] args) {
new Thread(instance1, "C-1").start();
new Thread(instance2, "C-2").start();
}
}
Despite using separate instances (instance1, instance2), both threads contend for the ClassLevelLock.class monitor — serializing access.
Explicit Class Object Locking
Equivalent behavior can be achieved via explicit synchronization on the Class literal:
synchronized (ClassLevelLock.class) {
// Critical section — same lock as static synchronized method
}
Underlying JVM Mechanism: Monitor Instructions
At the bytecode level, synchronized blocks compile into monitorenter and monitorexit instructions. Each object maintains a monitor counter (recursion depth):
- On entry:
monitorenterincrements the counter. If zero, the thread acquires the monitor; if already owned by the same thread, it increments (enabling reentrancy). - On exit:
monitorexitdecrements the counter. When it reaches zero, the moniter is fully released.
This mechanism inherently supports reentrant locking: a thread holding a lock can reacquire it without blocking.
Example demonstrating reentrancy:
public class ReentrantExample {
private synchronized void outer() {
System.out.println("outer called");
inner();
}
private synchronized void inner() {
System.out.println("inner called");
// No deadlock — same thread reenters its own lock
}
public static void main(String[] args) {
new ReentrantExample().outer();
}
}
Memory Visibility Guarantees
synchronized establishes a happens-before relationship between the release of a monitor and its subsequent acquisition by another thread. This ensures visibility of writes performed inside the synchronized block:
public class VisibilityDemo {
private int value = 0;
public synchronized void write() {
value = 42; // Write visible to next reader
}
public synchronized int read() {
return value; // Reads latest value written by previous writer
}
}
The unlock in write() happens-before the lock in read(), guaranteeing that read() observes value == 42 if invoked after write() completes.
JVM Lock Optimization Strategies
Modern JVMs (JDK 6+) apply multiple optimizations to reduce synchronization overhead:
| Optimization | Description |
|---|---|
| Biased Locking | Assumes single-threaded access; stores thread ID directly in object header. Eliminates CAS on first acquisition. Disabled by default since JDK 15. |
| Lightweight Locking | Uses CAS-based spinning instead of OS mutexes when contention is low. Avoids kernel transitions. |
| Adaptive Spinning | Adjusts spin count dynamically based on recent success/failure history of the same lock. |
| Lock Coarsening | Merges adjacent synchronized blocks on the same object into a single broader region. |
| Lock Elimination | Removes synchronization entirely when escape analysis proves the locked object never escapes the current thread’s scope. |
Lock State Transitions
Java monitors evolve through states based on contention:
Unlocked → Biased → Lightweight → Heavyweight
- Biased: Optimized for uncontended single-thread use.
- Lightweight: CAS-based spinning under light contention.
- Heavyweight: Falls back to OS-level mutexes (
pthread_mutex) under sustained contention.
State upgrades are one-way; downgrades do not occur.
Comparing synchronized and java.util.concurrent.locks.Lock
| Aspect | synchronized |
Lock Interface |
|---|---|---|
| Acquisition Control | Blocking only; no timeout or interruption support | tryLock(), tryLock(long, TimeUnit), lockInterruptibly() |
| Flexibility | Single implicit condition per lock | Multiple Condition instances per Lock |
| Explicitness | Automatic acquisition/release | Manual lock()/unlock() required (often in finally) |
| Fairness | Non-fair by default (may cause starvation) | Optional fairness policy via constructor |
| Performance | Highly optimized by JVM; minimal overhead in uncontended case | Slightly higher baseline cost but more predictable under contention |
Best Practices and Pitfalls
- Avoid null lock objects:
synchronized(null)throwsNullPointerException. - Prefer narrow scopes: Minimize synchronized regions to reduce contention.
- Never hold locks during I/O or long computations: Increases latency for other threads.
- Prevent deadlocks: Acquire multiple locks in consistent global order.
- Prefer high-level concurrency utilities:
ConcurrentHashMap,CopyOnWriteArrayList,CompletableFuture, etc., often eliminate the need for explicit locking.
synchronized remains the idiomatic choice for simple, well-scoped mutual exclusion due to its simplicity, safety, and deep JVM integration.