ReentrantLock principle

ReentrantLock

Basic Usage

java 复制代码
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:

java 复制代码
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

java 复制代码
// 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

java 复制代码
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:

java 复制代码
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:

java 复制代码
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()

java 复制代码
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:

java 复制代码
// 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).

java 复制代码
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:

java 复制代码
// 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.

java 复制代码
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

java 复制代码
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

java 复制代码
// 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:

java 复制代码
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

java 复制代码
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:

java 复制代码
ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();  // condition queue 1
Condition notFull  = lock.newCondition();  // condition queue 2

await() --- Full Flow

java 复制代码
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:

java 复制代码
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:

java 复制代码
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:

java 复制代码
// 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;

Connection to Other Docs

  • aqs-core.md --- AQS structure, CLH queue, acquire/release flow, waitStatus, cancelAcquire
相关推荐
还是奇怪1 分钟前
OpenAI GPT-5.6 构建指南拆解:创业公司如何用模型选择与 Responses API 降低 Agent 成本
java·数据库·人工智能·gpt
可涵不会debug2 分钟前
LangChain 文本分割器详解:字符分割、Token 分割、硬约束递归分割实战
java·前端·数据库
sbjdhjd6 分钟前
ThinkPHP 5.0.10 缓存写入型 RCE 复盘:从手动搭建、换行绕过到源码单步验证 | 05
java·后端·安全·spring·网络安全·数据挖掘·php
程序员清风23 分钟前
记忆系统设计:让 Agent 拥有短期记忆与长期记忆
java·服务器·网络
EatFan31 分钟前
前后端分离项目中 Token 到底应该怎么设计?Access Token + Refresh Token 实战
java·前端·后端
麻瓜code36 分钟前
[Agent]Spring AI 工具调用实战:让大模型真正“干活“(@Tool 六大工具 + 统一注册)
java·人工智能·spring
pen-ai38 分钟前
【优化方法】最小二乘:从误差平方到线性与非线性拟合
人工智能·算法·机器学习
vx-程序开发1 小时前
【毕设分享】springboot废品上门回收微信小程序---附源码94491
java·spring boot·毕业设计·源码·课程设计·毕设·大作业
宸津-代码粉碎机1 小时前
Spring AI 高危CVE漏洞深度复盘|生产禁跑版本汇总+临时防御+修复方案
java·大数据·人工智能·python·spring
敲代码的嘎仔1 小时前
用 Redis 合并写 + DelayQueue 延迟检测,把视频播放进度写库频率降到 1/60
java·开发语言·数据库·redis·缓存·音视频·高并发