深入ReentrantLock底层:AQS同步框架与锁实现全解析
1. 先从一次线上卡顿说起锁选择背后的代价大概半年前我们线上某个核心服务突然出现了一批超时告警。某个业务接口的TP99从2毫秒直接飙到800毫秒而且持续时间将近十分钟。查日志发现不是数据库问题不是下游依赖超时代码主链路就那么几行唯一可疑的是那段用了synchronized的临界区。当时那个方法里做的是缓存刷新加一个轻量级的统计逻辑锁的范围也不大刚开始大家都不太信是锁的问题。直到把线程dump拉下来才看到几十个线程全部BLOCKED在同一个synchronized块入口持锁线程又卡在了一次JVM的GC停顿上——就这么一顶多几十毫秒的停顿排队线程全堵在一起连锁反应把接口打垮了。那次之后团队认真讨论过要不要把热点路径的synchronized换成ReentrantLock。讨论过程中教材式的结论翻出来一堆“ReentrantLock更灵活”“支持超时、支持中断”“性能更好”。但说实话如果你只停留在会用API的层面换不换区别并不大。真正能指导决策的是搞清楚ReentrantLock底层到底怎么实现的——它背后是一套叫AbstractQueuedSynchronizerAQS的同步框架。这个东西是Java并发包的半壁江山ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock全靠它支撑。我把ReentrantLock的源码完整读了一遍之后很多之前觉得“玄学”的行为都有了明确答案为什么非公平锁在竞争激烈时性能反而好为什么lock不能被中断而lockInterruptibly可以为什么重入计数要小心int溢出这篇文章就把我啃源码的完整思路和踩过的认知坑一起梳理出来。如果你正在用ReentrantLock但没搞懂它内部怎么运作或者准备面试时面对“AQS原理”这类问题想答出深度这篇文章应该能帮你省下不少自己摸索的时间。2. AQS的三块基石state变量、CLH队列与模板方法AQS全称AbstractQueuedSynchronizer抽象队列同步器。它做的核心事情其实就三件记住当前锁被谁持有、记住哪些线程在排队、定义一套获取和释放的流程框架。理解这三件事整篇源码就可以拆开来看。2.1 state变量一个int如何完成加锁、重入、释放三件事AQS里有一个特别重要的字段private volatile int state;这个int就是整个同步状态的核心。在ReentrantLock的场景下state的含义是“当前锁被重入的次数”0表示没有线程持有锁1表示某线程拿了一次锁大于1表示同一个线程重入了N次需要释放N次才能真正把锁还回去。为什么要用volatile因为多个线程都可能读这个值写这个值改state的那条线程和读state的其他线程之间必须保证可见性不加volatile可能会出现持锁线程释放了锁等待线程看到的state还是1的极端情况。当然光有volatile还不够修改的时候还要配合CAS才能保证原子。state这个变量被设计成由子类自己定义语义AQS只提供getState/setState/compareAndSetState三个方法。这个设计非常巧妙同一个AQSReentrantLock用来表示持有次数Semaphore用来表示剩余许可证数量CountDownLatch用来表示剩余计数。一个通用int被不同子类赋予不同含义整个并发工具家族的骨架就这么统一了。2.2 自定义CLH队列头尾指针与节点状态当多个线程竞争同一把锁只有一个能拿到其他线程需要有个地方“排队”。AQS用的是自创的CLH锁队列变体本质上是一个双向链表。static final class Node { static final Node SHARED new Node(); static final Node EXCLUSIVE null; static final int CANCELLED 1; static final int SIGNAL -1; static final int CONDITION -2; static final int PROPAGATE -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; }每个节点对应一个等待线程prev和next形成链表。waitStatus是关键字段它的取值含义我建议记牢状态值名称含义1CANCELLED节点对应的线程已取消等待该节点会被清理出队-1SIGNAL表示当前节点的后继节点线程需要被唤醒当前节点释放锁时要去unpark后继节点-2CONDITION节点在条件队列中等待不是同步队列的等待节点-3PROPAGATE共享模式下唤醒需要向后传播0初始值新节点入队后的默认状态重点理解SIGNAL。AQS里“排队等待”并不是靠主动轮询而是每个节点在park自己之前先把自己的前驱节点waitStatus设为SIGNAL意思就是“老哥你释放锁的时候记得叫醒我”。这个前顾后盼的设计避免了一个节点释放锁之后不知道去唤醒谁的问题。另外注意队列的head节点是个哨兵节点它不存实际等待线程对应的线程已经拿到锁或者曾经拿到过锁。真正的等待线程从head.next开始。2.3 模板方法模式把不变的骨架和变化的规则分开AQS的核心设计思路是模板方法模式。它把“获取锁失败后入队、自旋、阻塞、唤醒”这套通用流程完全写死在acquire/release方法里把“什么条件下算获取成功”这个可变规则抽象成tryAcquire/tryRelease交给子类去实现。public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这段代码是模板方法的典型用法。tryAcquire返回true就直接拿到锁走人返回false就addWaiter入队然后acquireQueued自旋等待。整个排队、阻塞、唤醒的流程所有子类完全复用这一套。ReentrantLock的NonfairSync和FairSync都只重写tryAcquire一个不检查队列直接抢一个先检查队列再抢行为差异全在那一个方法里。理解了这个框架再看ReentrantLock就会非常清爽它不需要自己管理等待队列不需要自己处理park/unpark只需要告诉AQS“我的锁是否空闲”“怎么占用”“怎么释放”就行。3. 非公平锁的加锁链路从CAS抢锁到排队自旋3.1 lock()入口处那个被很多人忽略的if我们以非公平锁为例看ReentrantLock.lock()调下去会发生什么// java.util.concurrent.locks.ReentrantLock.NonfairSync final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }这里有一个非常容易忽略的细节lock()的第一步不是调用AQS的acquire而是直接尝试CAS把state从0改成1。如果成功当场拿到锁连排队流程都不用进。这就是“非公平”的第一个层面新来的线程可以无视队列里已经排了多时的老线程直接插队抢锁。CAS成功之后还要执行setExclusiveOwnerThread(Thread.currentThread())把“当前持锁线程”记录成自己。这个字段定义在AbstractOwnableSynchronizer里是后续重入判断的关键依据。如果CAS失败说明锁被人占着才走进acquire(1)的完整流程。3.2 acquire()的完整旅程与addWaiter入队操作acquire(1)是AQS的通用入口public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }执行顺序是这样三步。第一步调用子类实现的tryAcquire(1)。在非公平锁里这个方法叫nonfairTryAcquirefinal boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }它做两件事如果state是0说明锁刚好被释放趁没人注意再CAS竞争一次如果state不是0但当前线程就是持锁线程说明是重入直接state 1。这里可以看出tryAcquire里又一次“抢锁”所以非公平锁在锁释放瞬间的窗口期新线程和排队线程是同场竞技谁快是谁的。第二步如果tryAcquire返回false进入addWaiter(Node.EXCLUSIVE)把当前线程包装成Node节点加入等待队列尾部private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); Node pred tail; if (pred ! null) { node.prev pred; if (compareAndSetTail(pred, node)) { pred.next node; return node; } } enq(node); return node; }这里有个CAS设置尾节点的操作。因为多线程可能同时入队必须用CAS保证只有一个线程能把tail更新为自己的节点。第一次入队时 tail 为 null会走enq方法初始化一个哨兵head节点再通过自旋把节点挂上去这也是AQS里少数几处自旋CAS之一。第三步入队成功之后调用acquireQueued这是整个阻塞等待最核心的循环。3.3 acquireQueued循环自旋、park与中断final boolean acquireQueued(final Node node, int arg) { boolean failed true; try { boolean interrupted false; for (;;) { final Node p node.predecessor(); if (p head tryAcquire(arg)) { setHead(node); p.next null; failed false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }这个循环的设计很聪明。一个节点入队后不是傻等着而是先看自己的前驱节点是不是head。如果是head说明自己排在队首可以再尝试一次tryAcquire抢锁。如果抢到了把自己设为新的head然后把旧head的next置空帮助GC回收——这个出队过程没有用任何锁纯粹靠链表指针操作。如果前驱不是head或者抢锁失败就调用shouldParkAfterFailedAcquire(p, node)private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws pred.waitStatus; if (ws Node.SIGNAL) return true; if (ws 0) { do { node.prev pred pred.prev; } while (pred.waitStatus 0); pred.next node; } else { compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }这个方法的逻辑是先检查前驱节点状态如果是SIGNAL说明前驱承诺释放锁时唤醒自己那就可以安心的park如果是CANCELLED就不断向前跳跃跳过所有已取消的节点把无效节点从队列里摘除如果是其他状态尝试CAS把前驱改成SIGNAL。第一次循环通常不会直接park而是先把前驱状态改成SIGNAL然后第二次循环才真正进入parkAndCheckInterrupt阻塞当前线程。把SIGNAL约定透传下去释放锁的线程才能准确知道自己要唤醒谁。3.4 公平锁vs非公平锁hasQueuedPredecessors的微妙判断公平锁的tryAcquire和非公平的只在开头有一处差异protected final boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (!hasQueuedPredecessors() compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }多出来的就是hasQueuedPredecessors()public final boolean hasQueuedPredecessors() { Node t tail; Node h head; Node s; return h ! t ((s h.next) null || s.thread ! Thread.currentThread()); }这个方法判断“当前是否有线程比我先排队”。注意这个判断细节h t表示队列为空或者队列刚初始化完没人排队返回false可以抢。h ! t表示队里有人。取h.next如果为null说明正处于入队过程中另一个线程正在把节点挂到head后面为稳妥返回true新线程就不抢了如果h.next.thread不是当前线程说明排在队首的另有其人返回true不抢。只有当前线程恰好就是队首节点才允许它参与竞争。这保证了严格FIFO的公平性。但代价是每次锁释放后只能唤醒并让队首线程去抢无法让刚到的线程立刻拿锁多了至少一次上下文切换的开销。所以在公平锁模式下整体吞吐通常低于非公平锁。我个人的建议是除非业务有严格公平需求不然默认用非公平锁就好。4. 解锁与重入state递减的细节里藏着并发安全的答案4.1 tryRelease中的owner校验与锁计数归零解锁的入口是ReentrantLock.unlock()底层调AQS的release(1)public final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }tryRelease由ReentrantLock.Sync实现protected final boolean tryRelease(int releases) { int c getState() - releases; if (Thread.currentThread() ! getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free false; if (c 0) { free true; setExclusiveOwnerThread(null); } setState(c); return free; }有个细节很多文章没提tryRelease先计算c getState() - releases再判断当前线程是否是持锁线程。如果当前线程压根没持有锁比如A线程获取了锁B线程却调unlock()会抛IllegalMonitorStateException。这跟synchronized的行为一致——非持锁线程不能释放别人的锁。判断放前面还是放后面的差别在于先算c可能会出现负数但既然随后就抛异常这个负值不会写入state所以不影响正确性。不过这种先运算后验证的风格容易让读代码的人愣一下我刚开始看这里也有点迷惑。真正“释放锁”的动作发生在c 0的时候只有当重入计数完全归零才会把exclusiveOwnerThread置为null并把free标记为true表示锁真的空了。如果c ! 0说明还有重入计数没释放完锁仍然被当前线程持有只是重入计数减少了而已。4.2 unparkSuccessor为什么从tail往前找release里tryRelease返回true后会检查head.waitStatus ! 0。为什么是head因为head节点是当前释放锁的线程之前持有的那个哨兵节点如果它的waitStatus不是0说明它曾经被标记为SIGNAL或者包含其他需要清理的状态表示后继节点在等待被唤醒。接下来看unparkSuccessor的实现private void unparkSuccessor(Node node) { int ws node.waitStatus; if (ws 0) compareAndSetWaitStatus(node, ws, 0); Node s node.next; if (s null || s.waitStatus 0) { s null; for (Node t tail; t ! null t ! node; t t.prev) if (t.waitStatus 0) s t; } if (s ! null) LockSupport.unpark(s.thread); }注意它先尝试直接唤醒node.next但如果后继节点为null或者已经被CANCELLEDwaitStatus 0就会从tail开始向前遍历找到离head最近的一个有效节点去唤醒。为什么要从后往前找这是CLH队列删除节点的核心难点某个节点被取消后它的next指针可能还没有正确维护但prev指针一定能指向前驱所以从后往前遍历是最安全的。这也是为什么在这个队列里next的维护允许一定程度的滞后而prev必须可靠。我觉得这个从后往前找的细节特别值得欣赏——它说明AQS在并发环境下为了安全宁可多遍历几步也不依赖一个假设一定完备的next链。4.3 重入的实现与int溢出保护重入能力是ReentrantLock相对synchronized的一个重要增强。synchronized其实也支持重入但不可见ReentrantLock把重入计数直接暴露在state里所以支持getHoldCount()等诊断方法。每次重入state 1释放一层state - 1。这里有个隐藏风险重入次数无限增加int有朝一日会溢出变成负数。源码里对这种极端情况做了防御if (nextc 0) throw new Error(Maximum lock count exceeded);我自己实际测试过要触发这个异常得写一个递归上锁的代码正常业务里很难碰到但保持这种防御性编程的习惯是好的——并发库的代码连“重入两亿次”这种不现实的场景都考虑到了何况业务代码里的边界条件。重入的语义还会影响tryLock、lockInterruptibly等经过重入判断的方法语义如果当前线程已经持有锁再调tryLock()也是立刻成功的不会因为“锁已被自己占用”而失败。写业务代码时如果有人反映“我明明调了tryLock却必成功”多半是重入导致排查时可以往这个方向想。5. 阻塞唤醒机制LockSupport与线程状态切换5.1 park/unpark的语义与使用AQS里没有用Object.wait/notify而是用了一个更底层的工具类LockSupport。它提供两个静态方法LockSupport.park(); LockSupport.unpark(thread);park让当前线程进入WAITING状态不释放任何锁unpark允许指定线程恢复。它的语义很像一个“许可证”unpark相当于发一张许可park相当于消费一张许可。如果先调了unpark再调parkpark会直接返回不会阻塞。这个特点非常关键它比wait/notify更不容易丢失唤醒信号。注意park被打断时不会抛InterruptedException只是静默返回。AQS在parkAndCheckInterrupt里必须主动检查中断标记private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }这个设计是刻意的把“是否处理中断”的决策权向上抛让上层方法决定是直接响应中断还是忽略中断。5.2 中断策略lock()与lockInterruptibly的分野理解了park的中断行为才能理解lock()与lockInterruptibly()的区别。acquire里是这样处理中断的public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }acquireQueued返回的是interrupted布尔值如果有中断发生它返回true但注意——它不立刻响应中断而是把中断标记保留住等拿到锁之后由selfInterrupt()重新设置中断标记static void selfInterrupt() { Thread.currentThread().interrupt(); }也就是说lock()是“忽略中断式加锁”就算等待锁的过程中收到中断信号线程也继续排队等锁等锁到手之后再恢复中断状态让上层代码自己处理。而lockInterruptibly()走的路径是acquireInterruptiblypublic final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }doAcquireInterruptibly在检测到中断时会直接抛InterruptedException退出等待而不是继续排队。面试常问的“可中断锁”和“普通锁”的区别本质就在这两条路径对park返回后的中断标记处理方式不同。这个区别在真实场景里非常实用。比如某个线程长期排队等锁外部希望它能响应关闭信号尽快退出就应该用lockInterruptibly配合中断机制实现优雅取消而不是用lock硬等。5.3 为什么不直接用wait/notify有人会问既然都是阻塞线程wait/notify不也能实现吗有几个关键差异第一wait必须持有内置锁才能调用且调用后立即释放锁。AQS场景下等待锁的线程此刻并没有持有锁如果强行用wait语义对不上。第二wait/notify依赖对象监视器的状态容易发生信号丢失、过早唤醒等问题LockSupport的“许可”模型天然规避了这类问题。第三性能。LockSupport.park/unpark是不需要进入JVM监视器机制的开销更小。早期JDK里synchronized经过大量优化后性能已经逼近但LockSupport在可控性和灵活性上依然有优势。我个人的理解是AQS需要的是一个“纯粹的阻塞原语”不跟锁状态绑定不要求持有某个对象的监视器LockSupport恰好就是这种原语。用它AQS才能把这个原语灵活嵌入自己的队列逻辑中。6. 条件队列Condition等待/通知的另一套完整机制6.1 条件队列与同步队列的结构差异ReentrantLock.newCondition()创建的ConditionObject是AQS的内部类它维护了一个独立的单向队列——条件队列。回顾一下结构差异队列类型方向用途同步队列双向链表prev / next等待获取锁的线程队列条件队列单向链表nextWaiter等待特定条件成立的线程队列一个线程可以同时持有锁并在条件队列上等待吗不行。准确说Condition.await()会让当前线程先释放锁然后进入条件队列等待等被signal唤醒后它又会从条件队列移动到同步队列重新参与锁竞争。所以条件队列里的节点它的nextWaiter指向条件队列的下一个节点而不是next。6.2 await/signal的完整流程await()的执行流程可以拆成四步public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node addConditionWaiter(); int savedState fullyRelease(node); int interruptMode 0; while (!isOnSyncQueue(node)) { LockSupport.park(this); if ((interruptMode checkInterruptWhileWaiting(node)) ! 0) break; } // 被唤醒后重新acquire锁 if (acquireQueued(node, savedState) interruptMode ! THROW_IE) interruptMode REINTERRUPT; if (node.nextWaiter ! null) unlinkCancelledWaiters(); if (interruptMode ! 0) reportInterruptAfterWait(interruptMode); }第一步把当前线程包装成条件队列节点。第二步fullyRelease把当前线程持有的锁完全释放包括所有重入计数因为条件等待必须放弃整个锁否则其他线程进不来条件永远不会改变。第三步进入自旋检查isOnSyncQueue(node)如果当前节点不在同步队列说明还没被signal继续park一旦被signal节点会被移到同步队列循环退出。第四步重新调用acquireQueued去竞争锁拿到锁才算真正从await返回。signal()的核心是把条件队列头节点转移到同步队列尾部public final void signal() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first firstWaiter; if (first ! null) doSignal(first); }doSignal里转移节点的关键是transferForSignal它会把节点的waitStatus从CONDITION改为0然后通过enq把它插入同步队列尾部再设置前驱节点的SIGNAL状态。如果前驱已经取消就直接唤醒该节点线程。6.3 条件等待的经典陷阱signal丢失与中断用Condition做生产者-消费者模型时最经典的一个坑是如果一个线程await之后另一个线程调用了signal但此时这个线程还在await之前的准备阶段——比如还在addConditionWaiter过程中——那么signal事件可能找不到这个等待者导致该线程永远等下去。JUC的源码在这里处理得比较小心signal拿到的firstWaiter就是条件队列的队首只要节点真的已经在条件队列里signal就不会丢。但业务代码的坑在于如果使用了signal而不是signalAll当有多个线程等待同一条件时你可能只唤醒其中一个而那个线程遇到的条件恰好不满足它继续等待后其他同样满足唤醒条件的线程却没人通知。所以除非你能明确保证单等待者模型否则用signalAll更稳妥。另外await返回时线程会重新竞争锁这个竞争是阻塞式的即使被signal唤醒如果锁被其他线程占着await也不会立即返回而是等拿到锁之后才返回。所以从“被signal”到“await返回”之间可能有很长延迟。理解这个时序对排查条件变量相关的线上问题非常关键。7. 实战中定位锁问题方法论与工具7.1 用jstack抓死锁现场如果线上出现锁相关的问题第一件事不是改代码而是保留现场。JDK自带的jstack是最直接的工具jstack 12345 thread_dump.txt 21抓下来的线程dump里如果存在死锁会在底部打印类似下面这段内容Found one Java-level deadlock: A-thread: waiting to lock monitor 0x... waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync... B-thread: waiting to lock monitor 0x... waiting on java.util.concurrent.locks.ReentrantLock$NonfairSync...看到这个基本就可以确认锁顺序问题了。如果dump里没有明确的deadlock字眼但大量线程状态是WAITING或BLOCKED就要继续往java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject这类堆栈信息里看确认它们在等什么锁、持锁线程卡在哪。想把AQS的等待队列完整打出来可以在启动参数里加-Djava.util.concurrent.locks.ReentrantLock.fairfalse之类说实话没有这种参数但可以用JMX的ThreadInfo.getLockName()拿到锁对象名称再配合ThreadMXBean做分析。日常排障用jstack加上日志就足够覆盖大多数情况。7.2 tryLock超时 vs lockInterruptibly中断只停留在lock/unlock的用法上等于放弃了ReentrantLock一大半基本功。真正常用的两个进阶方法是if (lock.tryLock(3, TimeUnit.SECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } } else { // 获取失败的处理 }tryLock带超时时间适合“我不愿意无限等待”的场景。它底层也是走tryAcquire只是多了超时判断和中断响应拿到就拿到拿不到也不阻塞返回值false直接进入降级逻辑。lockInterruptibly()适合“等待锁的过程中希望响应外部取消”的场景。假设一个线程池执行批量任务任务里等待一把分布式协调锁此时服务要优雅关闭如果线程卡在lock()上中断也没反应关闭流程会被拖很久换成lockInterruptibly通过在线程池里调用shutdownNow发出中断线程就能从锁等待中退出。注意tryLock不带参数版本是个特例它永远不会阻塞拿不到就立即返回false而lockInterruptibly则会一直等直到拿到锁或被打断。7.3 性能对比的微观观察很多人关心synchronized和ReentrantLock到底谁快。结论其实是“看场景”低竞争下两者差距很小高竞争且锁持有时间短、释放非常频繁时非公平的ReentrantLock通常更优锁持有时间长、等待线程多时两者的瓶颈都在上下文切换和唤醒延迟上此时更多要考虑的是锁粒度而不是锁类型。我做过一个简单的压测对比8线程并发对一个计数器递增每次持锁时间极短。synchronized和ReentrantLock非公平锁的最终吞吐几乎一样但公平锁明显低一截。如果持有锁时间拉长到几十毫秒三者差异反而缩小。所以不要在选型上过度纠结真正影响系统性能的是锁的粒度和锁竞争次数而不是API本身。我的最终体会把ReentrantLock源码啃完一遍后我最大的感受是AQS并不是一个高不可攀的设计它就是把“排队”“阻塞”“唤醒”这三个所有锁都要解决的问题提炼成了一套可复用的模板。之后再去看Semaphore、CountDownLatch、ReentrantReadWriteLock会发现全是同一套骨架换的只是tryAcquireShared、tryReleaseShared这些具体规则。如果你也想从头读一遍源码我的建议是别追求一次全部啃完先按state - Node - acquire - release这条主线走画一张简单的队列状态图把几个核心方法的调用链用笔写下来比在IDE里跳来跳去要快得多。我读完最大的收获不是记住每个方法的实现而是理解了为什么每条代码路径要这样设计为什么先置SIGNAL再park为什么释放要校验owner为什么唤醒要从后往前找。这些“为什么”才是并发编程的真正底气。