资讯动态

ReentrantLock源码解析:从AQS到CLH队列的并发锁实现

发布时间:2026/10/6 21:26:21 来源:尧图企业网站定制
1. 从一次“抢座位”说起ReentrantLock到底在锁什么我在写并发代码时遇到过一个很有意思的场景办公室里只有一个会议室大家都在抢。有人进去开会了后面来的人就得在门外排队等候会议结束出来一个下一个才能进去。这本质上就是一把锁的完整生命周期。而ReentrantLock的底层实现其实就是用源码把这套“抢座位–排队–离开座位”的规则给写成了机器能读懂的指令。很多初学者第一次看ReentrantLock源码一打开就晕了满眼都是compareAndSetState、acquireQueued、LockSupport.park不知道这些方法串起来到底在干什么。其实你只要抓住一个核心主线——抢占与排队整个源码就能像看一场接力赛一样清晰。ReentrantLock是Java并发包java.util.concurrent.locks里的可重入锁实现。所谓“可重入”就是同一个线程可以多次获取同一把锁而不会把自己给锁死。这个特性的底层依赖的就是AQSAbstractQueuedSynchronizer这个抽象队列同步器。可以说AQS是整个JUC并发包的基石工具CountDownLatch、Semaphore、ReentrantLock、ReentrantReadWriteLock这些常见同步工具骨架部分全都建立在AQS之上。这篇文章我不会去逐行翻译源码注释而是想按照我自己读源码时的思路把ReentrantLock从“抢座位”到AQS队列的这条完整链路捋一遍。你会看到线程拿到锁、没拿到锁、排队等待、被唤醒、释放锁时每一步在代码里的真实路径长什么样。不同基础的读者都能从自己的视角找到有价值的部分刚入门的朋友可以借助“排队占座”的类比建立整体认知有经验的开发者则可以关注我标注的源码细节和设计考量。在我开始动手之前建议你手边准备好JDK8或以上版本的源码包IDE里能直接跳转就好。下面所有分析我都以JDK 8的ReentrantLock和AbstractQueuedSynchronizer源码为参照这是最通用、最稳定的一套实现理解之后再看JDK 17、21的版本基本是一通百通。2. lock()的秒懂拆解一次CAS抢不到座位就乖乖排队2.1 先认识AQS里那张“座位表”AQS内部维护了一个volatile int state字段这个字段在不同同步工具里含义不一样。在ReentrantLock里state就代表“锁被获取的次数”。state 0表示当前没人持有锁state 1表示锁被某个线程持有了如果同一个线程又重复获取了一次state就会变成2。这个设计是整个“可重入”特性的地基。同时AQS还维护了一个FIFO的双向链表队列我们通常叫它CLH队列原生的CLH锁用的是自旋AQS把它改造为阻塞唤醒的变体。这个队列里的每个节点是一个Node对象里面最重要的字段有三个thread当前排队的线程waitStatus当前节点的等待状态取值有1CANCELLED被取消、-1SIGNAL后继需要被唤醒、-2CONDITION在条件队列中、-3PROPAGATE共享锁传播和0初始状态prev、next前驱节点和后继节点我说它是“座位表”是因为这个队列并不保存已经拿到锁的线程而是专门用来存放那些抢锁失败、正在排队的线程。队头是一个特殊的“哨兵节点”它不存储真实线程纯粹是为了让队列的入队和出队操作有稳定的参照物。2.2 非公平锁的两次抢劫机会先看最常用的用法ReentrantLock lock new ReentrantLock(); lock.lock(); try { // 业务代码 } finally { lock.unlock(); }new ReentrantLock()默认创建的是非公平锁构造函数里sync new NonfairSync()。当某个线程调用lock()时走的是NonfairSync.lock()的逻辑final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }这里出现了传说中的“第一次抢劫机会”新来的线程根本不看队列里有没有人在排队直接尝试用CAS把state从0改成1。如果成功就说明锁是空闲的而且被我抢到了马上把自己设置为exclusiveOwnerThread锁的持有线程整个lock()调用结束。这里要解释一下为什么要用CAS而不是直接用if (state 0)的判断因为state是共享变量多个线程同时读到0接着同时把state改成1锁就被破坏了。CASCompare And Swap是一条CPU原子指令能保证“比较交换”这两个操作不可分割。Java里compareAndSetState(0, 1)底层走的是Unsafe.compareAndSwapInt在并发环境下这是最可靠的原子操作。如果CAS失败说明锁已经被别的线程占了或者锁刚被别的线程抢走那就老实进入acquire(1)。这个acquire方法在AQS里是模板方法它定义了一套从“再试一次”到“入队排队”再到“阻塞等待”的标准流程。public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这里还有个容易被忽略的细节进入acquire之后线程还没有立刻放弃而是会再尝试一次——这就是非公平锁的“第二次抢劫机会”。tryAcquire在NonfairSync里的实现核心逻辑是再CAS一次如果成功就直接拿锁返回如果失败就返回false然后去排队。两次抢不到才肯去排队这也是非公平锁吞吐量更高的原因它让新来的线程有插队的机会避免线程频繁在运行态和阻塞态之间切换。2.3 公平锁的“先来后到”公平锁FairSync的lock()就简单了直接进入acquire(1)没有开头那一下CAS。它的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的情况下哪怕新线程刚来也可以直接占锁。所以公平锁也是“当前没人排队才允许直接获取”严格来说不是绝对先来后到而是“先到先排队排队的第一位优先”。我很久以前被h ! t这个判断坑过一次当时觉得队列都有元素了为什么还要判断h.next null后来才明白这是为了处理一种中间状态当一个线程正在执行入队操作head和tail已经不再相等了但head.next可能还没来得及设置此时如果有一个新线程来抢锁它应当知道“队列里其实马上就会有人”所以h.next null也被视为“有人在排队”。这个细节只有看源码才能捕捉到。3. acquireQueued的等待循环从挂起线程到锁的“接力棒”传递3.1 addWaiter把线程挂上队尾第一次抢锁失败之后线程要做的事是addWaiter(Node.EXCLUSIVE)把自己打包成一个独占模式的节点放到队列末尾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把队尾指向新节点。这里又有一个经典问题为什么node.prev pred在CAS之前执行而pred.next node在CAS之后执行答案是设计上的巧妙之处。AQS允许在遍历队列时从tail往前遍历靠prev指针这样即使在并发入队的过程中队列结构也是“可回溯”的。而next指针的维护并不要求严格实时因为从尾部向前遍历时完全靠prev就能找到所有等待节点。如果CAS失败了说明有其他线程也在入队那就走enq的自旋入队逻辑private Node enq(final Node node) { for (;;) { Node t tail; if (t null) { if (compareAndSetHead(new Node())) tail head; } else { node.prev t; if (compareAndSetTail(t, node)) { t.next node; return t; } } } }enq里使用了标准的自旋CAS来处理并发冲突保证任何情况下队尾都能被正确挂上。首次队列为空时会先创建一个哨兵节点作为head和tail然后再挂真实节点。这个哨兵节点就是我在第2节说的“座位表”表头它让所有后续节点都有了一个稳定的prev参照。3.2 acquireQueued排队等待时线程到底在干什么节点挂到队尾之后就进入整个锁机制最核心的acquireQueued方法了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原来的头节点从链表中摘除完成“出队”。没抢到就检查自己是否可以安全阻塞然后调用LockSupport.park挂起。关键点在shouldParkAfterFailedAcquire里这个方法虽然名字看着复杂但干的事情很实际把前驱节点的waitStatus从0改成-1SIGNAL表示“我睡着之后前驱释放锁的时候要负责唤醒我”。这是park机制能正确运转的前提之一。第一次循环时前驱waitStatus通常还是0所以shouldParkAfterFailedAcquire会返回false不park而是再进一次循环。第二次进入时前驱状态已经是-1了这才真正返回true执行parkAndCheckInterrupt把线程挂起。这里有一个很反直觉的点为什么不能第一次循环就直接park而要空转一次假如我们一入队就park那么在前驱已经准备好释放锁而还没来得及设置SIGNAL的瞬间我们睡着就可能永远等不到唤醒死锁了。所以第二次循环就像是一种“确认对方答应叫我才可以睡”的协议这个细节是AQS设计的精妙之处。parkAndCheckInterrupt做了两件事调用LockSupport.park(this)挂起当前线程然后检查线程是否被中断过。LockSupport.park底层是Unsafe的park方法直接操作系统线程的挂起/恢复机制这也是为什么阻塞线程不会像synchronized那样产生“偏向锁膨胀”等一系列重量级锁状态切换性能特征完全不一样。3.3 头节点的交接为什么被唤醒的节点会成为新的头部当某个线程被前驱唤醒后它会在acquireQueued里继续循环。这时候它通常已经排在队伍第一个了前驱是head于是它尝试tryAcquire。如果锁真的空闲了它拿到锁然后执行setHead(node)private void setHead(Node node) { head node; node.thread null; node.prev null; }注意这里并没有把node.next置空。因为head本身变成了一个“新哨兵”它的thread字段被清空prev被清空等到别的线程来排队时这个节点就成了它们的前驱参照。原来的旧head节点则通过p.next null把它从链表中断开方便垃圾回收。整个过程就像排队买奶茶排在第一个的人买到后离开窗口他原本站着的位置就变成了“窗口前沿”后面的人往前挪一位。4. unlock()的连锁反应从state递减到唤醒下一个等待者4.1 tryRelease的“可重入次数归零”判定释放锁的入口是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是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第一行就检查了当前线程和exclusiveOwnerThread是否一致不一致直接抛IllegalMonitorStateException。这防止了一个线程去释放别人持有的锁避免了运行时异常无从查起。第二state不等于0时释放锁并不会唤醒队列里的人。举个例子一个线程把锁重入了3次state 3它调用3次unlock前两次只是把state减到2、1锁还是持有的只有第三次把state减到0free才变成true才会触发unparkSuccessor唤醒下一个等待线程。这正是“可重入锁”和“不可重入锁”在实现上的核心分野。4.2 unparkSuccessor的“特殊照顾”为什么从尾部往前找后继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找到后继就好了为什么还要从tail往前遍历原因就是前面说的next指针不是并发安全的它可能因为入队/出队的并发操作而暂时指向错误或为空。但prev指针的设计保证了从尾部向前遍历一定能找到所有存活节点即使中间有节点被取消排队也能跳过它们找到真正的下一个需要唤醒的线程。而且注意它要找的是waitStatus 0的节点也就是没有被取消的节点。CANCELLED节点的waitStatus 1会被直接跳过。这种从尾部向前找的方式还有另一个好处能选到离head最近的有效后继避免唤醒一个已经取消的线程导致锁无人接管的局面。5. 可达且可控可重入、中断响应与tryLock的源码级差异5.1 可重入的密码同一个线程为何能反复加锁可重入的实现前面已经反复提到了核心就藏在tryAcquire里else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) throw new Error(Maximum lock count exceeded); setState(nextc); return true; }只要当前线程就是锁的持有者无论state是多少直接把state累加上去即可不需要CAS。这是因为锁的持有权已经确认是当前线程不存在竞争关系普通setState就够了。有个细节值得注意int nextc是有可能溢出的。如果锁重入Integer.MAX_VALUE次就会变成负数所以源码里专门做了nextc 0的检查并抛出Error。虽然实际场景几乎不可能重入这么多层但JDK的老练之处就在于连这种极端边界都考虑到了。5.2 lockInterruptibly与lock()响应中断的差异synchronized有一个让很多开发者抱怨的点一个线程如果卡在锁上等待你没法安全地让它“别等了”。ReentrantLock则提供了中断响应的能力用lockInterruptibly()可以做到public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }AQS的acquireInterruptibly会在两个地方检查线程的中断标记尝试获取锁之前如果线程已经被中断直接抛InterruptedException不再尝试抢锁。在parkAndCheckInterrupt里如果被唤醒后发现中断标记为true会抛出InterruptedException退出排队。那么普通的lock()遇到中断会怎么样源码里acquireQueued会记录中断状态但不会立刻抛异常而是最终通过selfInterrupt()再补一次中断把中断状态重新设置到当前线程上。它选择的是“让线程继续排队抢锁但保留中断标记由业务代码自己去决定何时响应”。这两种策略的取舍很简单如果你希望等待锁的过程可以被打断就不要用lock()改用lockInterruptibly()。我自己的实践是在写线程池任务、外部服务调用这类可能因为下游慢而长时间阻塞的场景倾向使用带超时的tryLock(timeout, unit)等下我再细说。5.3 tryLock和时间版tryLock的“优雅失败”非阻塞的tryLock()走的是很直接的路子public boolean tryLock() { return sync.nonfairTryAcquire(1); }nonfairTryAcquire就是tryAcquire的非公平版本没有hasQueuedPredecessors判断试一次CAS成功就true失败就false。绝不含糊也不排队。而tryLock(long timeout, TimeUnit unit)走的是acquire的带超时版本在循环里会反复检查剩余时间如果过期仍未拿到锁就返回false并顺手执行cancelAcquire把节点状态设为CANCELLED。这个流程里最需要关注的就是doAcquireNanos对纳秒级超时精度的处理private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (nanosTimeout 0L) return false; final long deadline System.nanoTime() nanosTimeout; ... for (;;) { ... nanosTimeout deadline - System.nanoTime(); if (nanosTimeout 0L) return false; if (shouldParkAfterFailedAcquire(p, node) nanosTimeout spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); ... } }这里的spinForTimeoutThreshold是AQS里的一个常量static final long spinForTimeoutThreshold 1000L;它代表当剩余超时时间小于1微秒时不再使用park阻塞而是直接自旋等待。原因很容易理解线程的挂起和唤醒开销在微秒级别如果只剩几百纳秒就超时了再去park一次得不偿失不如空转一下。这是JUC里非常经典的“自适应自旋”优化思路。5.4 取消排队后队列是怎么自我修复的前面多次提到的cancelAcquire是所有“拿不到锁又不想等了”的出口。它在等待队列里做了一些手术private void cancelAcquire(Node node) { if (node null) return; node.thread null; Node pred node.prev; while (pred.waitStatus 0) node.prev pred pred.prev; Node predNext pred.next; node.waitStatus Node.CANCELLED; if (node tail compareAndSetTail(node, pred)) { compareAndSetNext(pred, predNext, null); } else { ... if (next ! null next.waitStatus 0) compareAndSetNext(pred, predNext, next); ... } }这段代码的意图我总结过一句话把取消的节点从队列里“摘除”但不完全摘除干净因为并发环境下强行改next指针可能会破坏正在进行的入队操作。所以源码采取的是兼容性做法——提高node.waitStatus为CANCELLED让后续遍历时能够跳过它同时尽量通过CAS把前驱节点的next指针指向跳过它的后继节点。这里有一个我踩过的实战坑如果业务里频繁用lockInterruptibly配合超时控制大量线程同时取消排队时队列里会堆积不少CANCELLED节点来不及时清理unparkSuccessor从尾部往前遍历时会多花一些时间。在极端高并发场景下这个遍历成本会累加需要我们自己去监控队列长度或者改用信号量等更轻量的方案。这条经验写在这里算是给做高性能架构的朋友提个醒。6. Condition条件队列为什么它和等待队列是两套体系6.1 await/signal的代码路径和底层数据结构很多人在理解Condition时最容易把它和AQS同步队列搞混。我打个比方同步队列是“抢会议室的人”条件队列是“等咖啡的人”。会议室和咖啡不是一回事等的人也互不干扰。每一把ReentrantLock可以创建多个条件对象ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition();每个Condition都对应一个独立的FIFO条件队列通过ConditionObject这个内部类实现。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); ... }addConditionWaiter把当前线程包装成CONDITION状态的节点挂到条件队列末尾。fullyRelease则做了一件和可重入有关的关键操作它会把锁释放掉并且记住之前重入的次数savedState。等线程被唤醒重新拿锁时需要重新获取这么多次的锁。否则一个重入3次的线程执行await如果只释放一次其他线程根本进不来。接着就是自旋检查isOnSyncQueue(node)条件队列里的节点不会自动跑到同步队列里只有其他线程调用了signal或signalAll节点才会被转移回同步队列。转移动作在transferForSignal里完成final boolean transferForSignal(Node node) { if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) return false; Node p enq(node); int ws p.waitStatus; if (ws 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) LockSupport.unpark(node.thread); return true; }它先把节点的CONDITION状态清掉然后enq把节点插入同步队列尾部再确保前驱节点状态是SIGNAL。如果前驱被取消了或者CAS失败就直接唤醒这个线程让它自己重新走一遍排队逻辑。整体来看signal不是直接把锁交给等待者而是“把等待者从咖啡区带回会议室门口重新排队”。6.2 signalAll的实现技巧把整队人一次性带走signalAll比signal多做的部分就是把条件队列里的所有节点全部转移到同步队列。它的实现不是逐个调用signal而是遍历条件队列依次执行transferForSignal。这里有源码里一个有趣的细节public final void signalAll() { if (!isHeldExclusively()) throw new IllegalMonitorStateException(); Node first firstWaiter; if (first ! null) doSignalAll(first); }条件队列在Node里维护了nextWaiter指针专门串起条件队列中的节点。虽然它也叫next但和同步队列的next完全不在一个维度上使用。我在读第一遍源码时发现Node的nextWaiter字段在多处复用作“独占模式标记”Node.EXCLUSIVE为null时就代表共享模式又承担条件队列的链表指针一度看得云里雾里。后来才理解这是JDK在内存占用上的妥协一个字段干两件事靠语义区分。6.3 一个典型的Condition使用模板放到实际项目里最常见的Condition用法是生产者消费者模式。我这里给一个我自己常用的模板代码结构清晰直接可以抄ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition(); QueueString queue new ArrayDeque(); int capacity 10; // 消费者 public String take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } String item queue.poll(); notFull.signalAll(); return item; } finally { lock.unlock(); } } // 生产者 public void put(String item) throws InterruptedException { lock.lock(); try { while (queue.size() capacity) { notFull.await(); } queue.offer(item); notEmpty.signalAll(); } finally { lock.unlock(); } }这里有两个重要原则第一await的使用必须放在循环里不能只用一次if判断因为线程被唤醒后条件不一定真的满足了存在“虚假唤醒”的可能只有循环检查才能保证安全。第二signal和signalAll必须在持有锁的情况下调用源码里isHeldExclusively()已经强制了这个约束违反它直接抛异常。7. 高频学习问题的源码级回答与我的经验补充7.1 为什么说AQS队列是CLH锁的变体它和原始CLH的区别在哪原始CLH队列锁的思路是每个等待线程在前驱节点的某个字段上自旋前驱释放锁时改状态后面的线程感知到状态变化后自旋结束。这在多核CPU上有效但自旋会浪费CPU资源。AQS把“自旋等待”升级成了“阻塞等待”同时保留了CLH的队列结构。它的关键转变在于线程不会一直自旋而是通过LockSupport.park挂起只有前驱释放锁时才会被unpark唤醒。所以AQS严格来说是一种“基于哨兵节点双向链表的阻塞式CLH变形”。理解这一点很多关于AQS的迷惑都会解开比如为什么需要waitStatus来标记状态、为什么要维护prev和next双向指针。7.2 非公平锁真的“完全不管排队的人”吗非公平锁的“非公平”体现在抢锁那一刻它允许新线程插队。但要注意它并不是任何时刻都能插队一旦插队失败进入同步队列它就是队列中的普通一员只能按顺序等待。更重要的是非公平锁在tryAcquire里获取锁的条件其实包含了一种隐式让步——如果当前持有锁的线程刚好是当前线程它可以重入如果锁空闲它就直接插队。所以真正的效果是锁空闲时新线程有插队机会锁被持有时大家一样排队。7.3 锁性能调优时我如何选择公平锁还是非公平锁从源码层面对比下来我的倾向很明显默认场景用非公平锁。非公平锁的两次CAS尝试减少了一半以上的线程park/unpark开销吞吐量更高。公平锁呢适合那种对“等待时间公平性”有硬性要求的业务比如FIFO队列的调度任务要防止某个线程被无限期饿死。但从我这些年线上系统的实践来看真正的性能瓶颈很少出在锁本身的公平性上而是由于锁的粒度太大、持锁时间过长。调整锁粒度、减少临界区代码收益远大于纠结公平与非公平。另外读过源码之后我建议你在压测环境里顺手做一件事打印一下AQS队列深度。JDK里获取当前排队线程数可以用ReentrantLock.getQueueLength()如果发现队列深度经常超过两位数说明锁竞争已经很严重了这时候更该去考虑拆分锁、引入ConcurrentHashMap等无锁结构或LongAdder这类分散竞争的工具。7.4 我当年读源码时最容易绕晕的三个地方第一unlock()和await()的联合使用调用await()后锁被释放其他线程可以进入临界区但当前线程从await()返回时它已经重新拿回了锁并且恢复了自己原本的重入次数。这一来一回之间千万不要以为锁一直没被释放过。第二waitStatus状态值的正负很容易看反。负值代表“有意义的状态”SIGNAL、CONDITION、PROPAGATE正值只有一个CANCELLED(1)代表“这个节点已经放弃了”。所以源码里厉害的判断都是 0就跳过取消节点 0就要尝试状态转移。第三Node的nextWaiter字段到底在表示什么。我建议直接把“同步队列里的节点”和“条件队列里的节点”看成两种不同上下文下的角色同一个Node对象在不同时刻可以切换身份nextWaiter的含义也随之变化千万不要用一个固定的语义去套所有地方。我对ReentrantLock源码最大的感受是AQS把“数学证明”级别的并发正确性藏在了看起来并不特别复杂的几百行代码里。阅读时像跟着一条河走看起来平静但每一处弯道都有它存在的必然理由。希望大家读完这篇拆解之后能自己再翻一遍源码你会有很多新的发现。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑