ReentrantLock vs synchronized: Source-Code Comparison & Selection Guide
This article provides a comprehensive source-code level comparison of Java's ReentrantLock and synchronized, covering their underlying implementations (JVM monitor vs AQS/CLH), lock upgrade mechanisms, fairness, interruptibility, timeout support, condition variables, and performance characteristics across JDK versions to guide practical selection.
Introduction
Java concurrency developers face a daily choice between synchronized and ReentrantLock. Common opinions claim synchronized is simpler and JVM-optimized, while ReentrantLock offers advanced features like fairness, interruption, timeouts, and multiple conditions. Some argue ReentrantLock outperforms synchronized under high contention. This article examines the source-level implementations, core differences, and performance realities to provide a principled selection guide.
1. Essential Differences
synchronized : JVM built-in monitor lock (C++ implementation), based on object header Mark Word and ObjectMonitor. Implicit locking — entering a synchronized block acquires the lock automatically; exiting releases it automatically.
ReentrantLock : JDK implemented reentrant lock (Java implementation), based on AQS (AbstractQueuedSynchronizer). Explicit locking — manual lock() / unlock() required, typically in a finally block.
This fundamental distinction — VM-level vs JDK-level — drives all subsequent differences.
2. synchronized Internals: Object Header & Lock Upgrade
2.1 Object Header: Where Lock State Lives
Every Java object has an Object Header containing a Mark Word storing hash code, generational age, and lock information. On 64-bit JVM (unlocked state):
| 25 bits | 31 bits | 1 bit | 4 bits | 1 bit | 2 bits |
| unused | hashCode| unused| age | biased| lock tag |The last two bits (lock tag) indicate lock state:
01 : Unlocked / Biased (biased bit = 1 for biased, 0 for unlocked)
00 : Lightweight — Mark Word points to stack lock record
10 : Heavyweight — Mark Word points to ObjectMonitor
11 : GC mark
Lock metadata resides in the object header Mark Word; no extra object needed.
2.2 Lock Upgrade: From Unlocked to Heavyweight
Since JDK 6, synchronized uses a one-way upgrade path based on contention:
Unlocked → Biased → Lightweight → Heavyweight
(contention increases, one-way, no downgrade)Phase 1: Unlocked
Newly created object, no thread contention.
Phase 2: Biased Locking
First thread entering synchronized block sets Mark Word to biased state recording its thread ID. Subsequent entries by same thread only compare thread ID — no CAS needed. Ideal for single-thread repeated access; near-zero overhead. Note : Biased locking disabled by default since JDK 15 ( -XX:+UseBiasedLocking no longer default), deprecated in JDK 18 because revocation overhead under high contention outweighs benefits.
Phase 3: Lightweight Locking
When a second thread attempts to acquire a biased lock, bias is revoked and lock upgrades to lightweight. Steps:
Thread creates a Lock Record in its stack frame.
CAS copies Mark Word to Lock Record and updates Mark Word to point to Lock Record.
CAS success → lightweight lock acquired.
CAS failure → contention detected; spin retry a few times, then upgrade to heavyweight.
Suitable for alternating low-contention access; avoids kernel transitions via CAS + spinning.
Phase 4: Heavyweight Locking
After spinning fails, lock becomes heavyweight based on ObjectMonitor (C++ object) containing: _owner: current lock-holding thread _EntryList: blocked threads waiting for lock _WaitSet: threads waiting via
wait() _count: reentry count
Lock/unlock invoke OS mutex, causing user/kernel mode switches — expensive but avoids CPU spinning under heavy contention.
2.3 Bytecode Level: monitorenter/monitorexit
synchronizedcompiles to two bytecode instructions:
0: aload_0
1: dup
2: astore_1
3: monitorenter ← enter monitor (acquire)
4: aload_1
5: monitorexit ← exit monitor (release, normal path)
6: goto 14
9: astore_2
10: aload_1
11: monitorexit ← exit monitor (release, exception path)
12: aload_2
13: athrow
14: returnTwo monitorexit instructions guarantee release on both normal and exceptional exits — JVM ensures no deadlock from forgotten unlock.
3. ReentrantLock Internals: AQS & CLH Queue
3.1 AQS Foundation
ReentrantLockdelegates to inner Sync class extending AbstractQueuedSynchronizer (AQS). AQS core components:
state : volatile int representing reentry count (0 = unlocked, n = reentered n times)
CLH Queue : failed acquirers wrapped as Nodes in a doubly-linked list, parked via LockSupport.park() ConditionObject : supports multiple condition queues
Two concrete sync implementations:
NonfairSync (default): lock() immediately CAS state from 0 to 1; on failure enters AQS queue.
FairSync : lock() directly enqueues; tryAcquire checks hasQueuedPredecessors() before CAS.
3.2 Nonfair Lock Acquisition Flow
CAS compareAndSetState(0, 1) — barge attempt; success sets owner thread and returns.
On failure, call AQS acquire(1). acquire invokes tryAcquire (via nonfairTryAcquire) for another barge attempt; success returns.
Failure → addWaiter wraps thread as Node appended to CLH queue tail. acquireQueued spins: if predecessor is head, retry CAS; else park() to suspend.
On unpark, thread re-spins and becomes new head upon success.
3.3 Unlock Flow
release(1)calls tryRelease(1): decrement state; if reaches 0, clear owner and return true.
If release succeeded (state=0), unparkSuccessor(head) wakes head's successor.
Woken thread returns from park, spins to acquire lock.
3.4 Reentrancy Implementation
Both support reentrancy via a counter: synchronized: ObjectMonitor _count increments on reentry, decrements on exit; lock released only when count reaches 0. ReentrantLock: AQS state increments in tryAcquire when current thread is owner; decrements in tryRelease; release only when state becomes 0.
Same principle: a counter tracks recursion depth.
4. Six-Dimension Comparison
4.1 Lock Acquisition
synchronized (heavyweight) : monitorenter → check ObjectMonitor._owner → if self, _count++; else enter _EntryList, block via OS mutex.
ReentrantLock (nonfair) : lock() → CAS state (barge) → on failure acquire → tryAcquire → enqueue Node in CLH queue → park.
Key difference : synchronized queue is JVM C++ _EntryList (invisible to Java); ReentrantLock queue is Java CLH list, monitorable via getQueueLength(), getWaitingThreads().
4.2 Lock Release
synchronized : monitorexit → _count-- → if 0, wake _EntryList head. Automatic — JVM guarantees release even on exception.
ReentrantLock : unlock() → release(1) → state-- → if 0, unpark CLH head successor. Manual — must call in finally or risk deadlock.
4.3 Fairness
synchronized : Only nonfair. After heavyweight upgrade, woken threads re-compete, allowing barging.
ReentrantLock : Supports both. Fair mode uses hasQueuedPredecessors() to enforce FIFO; nonfair (default) barges via CAS.
Fairness cost : Lower throughput (10-30% less), more context switches. Use only when starvation prevention is required.
4.4 Interrupt Responsiveness
synchronized : Cannot be interrupted while waiting; thread blocks indefinitely.
ReentrantLock : lockInterruptibly() throws InterruptedException, enabling cancellation (e.g., user cancel, timeout fallback).
4.5 Timeout Acquisition
synchronized : No timeout or try-lock support.
ReentrantLock : tryLock() immediate non-blocking attempt; tryLock(timeout, unit) waits up to specified duration. Enables fallback strategies (return cache, default value).
4.6 Condition Variables
synchronized : Single condition per object via wait() / notify() / notifyAll() on ObjectMonitor._WaitSet. notifyAll wakes all waiters — thundering herd problem.
ReentrantLock : Multiple Condition objects via newCondition(), each with own AQS ConditionObject queue. Example producer-consumer with separate notFull and notEmpty conditions avoids waking same-type threads.
5. Performance Comparison
5.1 JDK Version Impact
JDK 5 and earlier : synchronized heavyweight only; ReentrantLock clearly faster.
JDK 6 : synchronized introduces biased/lightweight/heavyweight upgrade; gap narrows.
JDK 8+ : Performance difference minimal; each wins in different scenarios.
JDK 15+ : Biased locking disabled by default; synchronized goes straight to lightweight under low contention, still very fast.
5.2 Contention Level Comparison
No contention (single thread) : synchronized slightly faster (biased/lightweight near-zero overhead); ReentrantLock has AQS framework overhead.
Low contention (alternating) : Roughly equal — both use CAS + spinning.
High contention (many threads) : ReentrantLock slightly faster — CLH queue enables precise wakeups; synchronized heavyweight also well-optimized.
5.3 Conclusion
Do not choose ReentrantLock for performance — modern JDK gap is negligible; premature optimization is harmful.
Functional requirements drive selection: need fairness, interruptibility, timeout, multiple conditions, monitoring → ReentrantLock; otherwise → synchronized. synchronized advantages: simplicity, safety (auto-release), continuous JVM optimization, cleaner code.
Summary & Selection Guide
ReentrantLockand synchronized are the two most common Java mutexes. Their essence differs: synchronized is a JVM built-in monitor lock (C++, object header + lock upgrade), while ReentrantLock is a JDK implemented reentrant lock (Java, AQS + CLH queue) .
Key Takeaways
synchronized internals : Mark Word stores lock state; upgrade path Unlocked→Biased→Lightweight→Heavyweight (one-way); heavyweight uses ObjectMonitor ( _owner, _EntryList, _WaitSet, _count); bytecode monitorenter / monitorexit ensures auto-release on exception.
ReentrantLock internals : AQS state tracks reentry count; nonfair lock barges via CAS then CLH queue; fair lock checks hasQueuedPredecessors(); unlock decrements state and unparks successor; ConditionObject enables multiple condition queues.
Six-dimension comparison : auto vs manual release; nonfair-only vs fair/nonfair; no interrupt vs lockInterruptibly(); no timeout vs tryLock(timeout); single condition vs multiple conditions; invisible queue vs monitorable queue.
Performance : JDK 8+ difference small; low contention favors synchronized, high contention slightly favors ReentrantLock; never select based on performance alone.
Selection : Simple sync, no advanced features → synchronized; need fairness/interrupt/timeout/multi-condition/monitoring → ReentrantLock; read-heavy → ReentrantReadWriteLock or StampedLock. synchronized and ReentrantLock are not replacements but complementary tools. Understanding their internals enables precise, scenario-driven choices for safe and efficient concurrent code. There is no best lock, only the most suitable lock.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Tech Workshop
Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
