ReentrantLock Mechanism
ReentrantLock
Basic Usage
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// critical section
} finally {
lock.unlock(); // MUST be in finally
}
Reentrant Counter
Same thread can acquire multiple times. Lock released when counter hits 0:
lock.lock(); // count = 1
lock.lock(); // count = 2
lock.unlock(); // count = 1 (still held)
lock.unlock(); // count = 0 → released
Forgetting one unlock() → lock never released → deadlock. Always use try/finally.
How Reentrancy Works in AQS
// NonfairSync.tryAcquire (simplified)
boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (CAS(state, 0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
// Reentrant — same thread, just increment
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc); // no CAS needed — only owner writes
return true;
}
return false;
}
No CAS for reentrant increment — only the owner thread can reach that branch, so a plain write is safe.
Fair vs Unfair
ReentrantLock unfair = new ReentrantLock(); // default: unfair
ReentrantLock fair = new ReentrantLock(true); // fair
Inheritance Chain
ReentrantLock.Sync ← tryAcquire, tryRelease
├── NonfairSync ← tryAcquire allows barging
└── FairSync ← tryAcquire checks hasQueuedPredecessors()
tryAcquire — Unfair (Default)
Can steal from queued threads:
boolean tryAcquire(int acquires) {
if (state == 0 && CAS(state, 0, acquires)) {
owner = currentThread; return true; // steal!
}
if (currentThread == owner) { state += acquires; return true; }
return false;
}
tryAcquire — Fair
Checks queue first:
boolean tryAcquire(int acquires) {
if (state == 0 && !hasQueuedPredecessors() && CAS(state, 0, acquires)) {
owner = currentThread; return true; // only if nobody waiting
}
if (currentThread == owner) { state += acquires; return true; }
return false;
}
Unfair (default) — incoming thread can steal the lock even if others are queued:
Queue: [T-1 parked] → [T-2 parked]
T-3 arrives, lock just released:
Unfair: T-3 CAS succeeds → T-3 gets lock (T-1 stays parked)
Fair: T-3 checks hasQueuedPredecessors() → true → T-3 enqueues behind T-2
Why unfair is faster: waking a parked thread costs ~1-10 μs (OS context switch). If T-3 can grab the lock immediately via CAS (~10-20 ns), it avoids that cost entirely. The parked thread will get its turn eventually.
Unfair can cause starvation under extreme contention — one thread keeps stealing. Fair guarantees FIFO ordering but is ~2-3x slower due to forced context switches.
lockInterruptibly()
lock.lockInterruptibly(); // throws InterruptedException if interrupted while waiting
try {
doWork();
} finally {
lock.unlock();
}
Difference from lock():
lock(): interrupt → remember → keep waiting → got lock → restore flag
lockInterruptibly(): interrupt → cancel → throw InterruptedException → no lock
Use lockInterruptibly() when you need to support cancellation (e.g., Future.cancel(true)).
tryLock() — Non-Blocking Acquisition
tryLock() — No Args, Instant
One CAS attempt, returns immediately. Never touches the AQS queue:
// Internals — that's the entire implementation
if (state == 0 && CAS(state, 0, 1)) return true;
return false;
No enqueue, no park, no waiting. Either the lock is free right now and you grab it, or you get false. Always unfair — ignores the queue even on a fair lock (by design, per Javadoc).
if (lock.tryLock()) {
try { doWork(); }
finally { lock.unlock(); }
} else {
doSomethingElse();
}
tryLock(timeout, unit) — Timed
Enters the AQS queue if CAS fails, but with a deadline:
// Internals (simplified)
long remaining = deadline - System.nanoTime();
if (remaining <= 0) { cancelAcquire(node); return false; } // time's up
if (remaining > spinThreshold)
LockSupport.parkNanos(remaining); // sleep until deadline
Flow: tryAcquire → fail → enqueue → loop of (tryAcquire / check deadline / parkNanos). If deadline passes, calls cancelAcquire(node) to cleanly remove from queue and returns false.
The spin threshold (~1000 ns) avoids parking for tiny remaining times — OS park/unpark overhead (~3-10 μs) would overshoot the deadline. For sub-microsecond remainders, it just spins.
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try { doWork(); }
finally { lock.unlock(); }
} else {
throw new TimeoutException("too slow");
}
tryLock() vs lock() vs lockInterruptibly()
| Queue? | Parks? | Interrupt | Timeout | |
|---|---|---|---|---|
tryLock() |
No | No | Ignored | Instant return |
tryLock(timeout) |
Yes | Yes (parkNanos) | Throws InterruptedException | Respects deadline |
lock() |
Yes | Yes (park) | Swallowed, restored after acquire | Never |
lockInterruptibly() |
Yes | Yes (park) | Throws InterruptedException | Never |
Deadlock Prevention with tryLock
while (true) {
if (lockX.tryLock()) {
try {
if (lockY.tryLock()) {
try { doWork(); return; }
finally { lockY.unlock(); }
}
} finally { lockX.unlock(); }
}
Thread.sleep(random); // back off, retry
}
Common Patterns
// Skip duplicate work (cache refresh)
if (refreshLock.tryLock()) {
try { refreshCache(); }
finally { refreshLock.unlock(); }
}
// Timeout for SLA-sensitive code
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try { callDownstream(); }
finally { lock.unlock(); }
} else {
return fallbackResponse();
}
Multiple Conditions — Producer-Consumer
With separate conditions, you wake exactly the right thread type:
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// Producer
lock.lock();
try {
while (queue.size() == capacity)
notFull.await(); // park in notFull condition queue
queue.add(item);
notEmpty.signal(); // wake ONE consumer
} finally { lock.unlock(); }
// Consumer
lock.lock();
try {
while (queue.isEmpty())
notEmpty.await(); // park in notEmpty condition queue
Item item = queue.remove();
notFull.signal(); // wake ONE producer
} finally { lock.unlock(); }
Compare with synchronized which has only one wait set per object — notifyAll() wakes ALL waiters (producers AND consumers), most go right back to sleep. With Condition, signal() wakes exactly one thread of the right type.
The Lock Is a Traffic Controller
lock() → count > 0 and I'm owner? → count++ (reentrant, no wait)
→ count == 0? → count = 1, I'm owner (no wait)
→ someone else owns? → park in sync queue (WAIT)
unlock() → count-- → still > 0? → nothing (reentrant)
→ count == 0? → unpark next in sync queue
await() → save count → count = 0, owner = null → move to condition queue → park
signal() → move head of condition queue → sync queue
wakeup → re-acquire lock → restore count → continue after await()
tryRelease — Reentrant Decrement
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // state - 1 (no CAS — only owner calls this)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException(); // not the owner!
boolean free = false;
if (c == 0) { // fully released
free = true;
setExclusiveOwnerThread(null); // clear owner BEFORE setState
}
setState(c); // volatile write → memory barrier
return free; // true only when state reaches 0
}
state = 3 → tryRelease(1) → state = 2 → return false (nobody woken)
state = 1 → tryRelease(1) → state = 0 → owner = null → return true → wake
Owner is cleared BEFORE setState because setState is a volatile write (memory barrier) — everything before it is flushed and visible. If state were set to 0 first, another thread could CAS(0,1) and see stale owner.
Condition Queue Internals
Each lock.newCondition() creates a separate singly-linked condition queue:
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition(); // condition queue 1
Condition notFull = lock.newCondition(); // condition queue 2
await() — Full Flow
void await() {
Node node = addConditionWaiter(); // add to condition queue (waitStatus = CONDITION)
int saved = fullyRelease(node); // release lock fully
while (!isOnSyncQueue(node))
LockSupport.park(this); // park until signal()
acquireQueued(node, saved); // re-acquire in sync queue with saved count
}
fullyRelease drops all reentrant levels in one shot:
int fullyRelease(Node node) {
int savedState = getState(); // e.g., 3 (locked 3 times)
if (release(savedState)) // tryRelease(3) → state = 0, owner = null
return savedState; // return 3 so await can restore later
throw new IllegalMonitorStateException();
}
After signal + re-acquire, acquireQueued(node, 3) restores the reentrant depth:
lock() → state = 1
lock() → state = 2
lock() → state = 3
await() → saved = 3, state = 0, owner = null ← fully released
... parked in condition queue ...
signal() → moved to sync queue
acquireQueued(node, 3) → CAS(0, 3), owner = me ← restored to 3
signal() — Transfer from Condition Queue to Sync Queue
signal() does NOT wake the thread. It moves the node between queues:
void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException(); // must hold the lock
Node first = firstWaiter; // head of condition queue
if (first != null)
transferForSignal(first);
}
boolean transferForSignal(Node node) {
// Step 1: CAS waitStatus from CONDITION(-2) to 0
if (!CAS(node.waitStatus, CONDITION, 0))
return false; // node was cancelled
// Step 2: Enqueue into sync queue tail
Node p = enq(node); // returns predecessor
// Step 3: Ensure predecessor will wake us
int ws = p.waitStatus;
if (ws > 0 || !CAS(p.waitStatus, ws, SIGNAL))
LockSupport.unpark(node.thread); // safety net — wake now
return true;
}
Step 3 safety net — two cases trigger immediate unpark:
ws > 0: predecessor is CANCELLED — it’s dead and will never unpark anyone- CAS failed: predecessor’s waitStatus changed concurrently (likely just got cancelled)
In both cases, we can’t rely on the predecessor to wake us, so unpark now. The thread wakes into acquireQueued, tries tryAcquire, and if it fails, shouldParkAfterFailedAcquire skips cancelled predecessors and re-parks safely.
Normal path: predecessor alive, CAS succeeds → predecessor has SIGNAL set → it will unpark us when it releases → no immediate unpark needed.
signal() Timeline
Thread-0 holds lock, Thread-1 in notEmpty condition queue:
Condition Queue: [Thread-1] → [Thread-2]
Sync Queue: head → [Thread-3]
Thread-0 calls notEmpty.signal():
1. Remove Thread-1 from condition queue head
2. CAS waitStatus: CONDITION(-2) → 0
3. enq(Thread-1) → append to sync queue tail
4. Set predecessor (Thread-3) waitStatus to SIGNAL
Condition Queue: [Thread-2]
Sync Queue: head → [Thread-3 SIGNAL] → [Thread-1]
↑ still parked!
Thread-0 calls unlock():
→ release → unpark Thread-3
Thread-3 acquires → becomes head → unlocks → unpark Thread-1
Thread-1 finally wakes, acquires lock, restores reentrant count
signalAll() does the same for every node in the condition queue.
Lock Acquisition Strategies
All four use the same AQS queue. The difference is the waiting policy:
// lock() — block forever
LockSupport.park();
Thread.interrupted(); // clear flag, keep waiting
// lockInterruptibly() — give up on interrupt
LockSupport.park();
if (Thread.interrupted()) { cancelNode(node); throw new InterruptedException(); }
// tryLock(timeout) — give up on deadline
long remaining = deadline - System.nanoTime();
if (remaining <= 0) { cancelNode(node); return false; }
if (remaining > spinThreshold) LockSupport.parkNanos(remaining);
// tryLock() — don't block at all
if (state == 0 && CAS(state, 0, 1)) return true;
return false;
Memory Ordering — How ReentrantLock Provides Happens-Before
ReentrantLock provides the same visibility guarantee as synchronized, but through a different mechanism: the AQS volatile int state field.
The Mechanism
lock.lock():
→ tryAcquire() → getState() // volatile READ (or CAS = volatile read+write)
// acquire barrier: sees all writes before the last unlock
lock.unlock():
→ tryRelease() → setState(0) // volatile WRITE
// release barrier: flushes all writes before this point
The JMM rule: a volatile write happens-before every subsequent volatile read of the same variable. Since unlock() writes state and lock() reads state, all writes before the unlock are visible after the lock.
Thread-A: Thread-B:
lock.lock();
data = 42;
list.add("item");
lock.unlock();
→ setState(0) [volatile write]
lock.lock();
→ getState() [volatile read / CAS]
assert data == 42; // guaranteed ✓
assert list.contains("item"); // guaranteed ✓
lock.unlock();
Why No CAS Needed on Unlock
tryRelease uses setState(nextc) — a plain volatile write, not CAS. This is safe because the current thread holds the exclusive lock. No other thread can be modifying state concurrently. The volatile write still provides the release barrier (flushes all prior writes to main memory).
Comparison with synchronized
synchronized |
ReentrantLock |
|
|---|---|---|
| Acquire barrier | monitorenter (JVM native) | CAS or volatile read of AQS state |
| Release barrier | monitorexit (JVM native) | volatile write of AQS state |
| JMM rule | “monitor unlock HB monitor lock” | “volatile write HB volatile read” |
| End result | Identical visibility guarantee | Identical visibility guarantee |
Both provide the same happens-before semantics. The difference is purely implementation: synchronized uses JVM-level monitor operations, ReentrantLock piggybacks on volatile semantics of the AQS state field.
See java-volatile-and-memory-model.md — Happens-Before Across Lock Types for the full comparison across all lock types.
更多推荐



所有评论(0)