深入ReentrantLock:AQS、可重入与公平锁非公平锁机制详解 Java并发这条线我一直觉得ReentrantLock是绕不开的一道坎。JUC包里面一堆工具类CountDownLatch、Semaphore、ThreadPoolExecutor底层全都跟AQS打交道而ReentrantLock又是AQS最经典、最纯粹的一个落地案例。很多朋友面试前背了一堆“可重入”“公平锁非公平锁”的概念可真被问到“state字段怎么实现可重入”“非公平锁到底在哪里插队”就卡住了。这篇就把ReentrantLock从使用到源码再到面试题一次讲透适合正在准备面试的Java开发也适合想搞懂锁底层机制的进阶学习者。1. 先搞清楚面试官想考什么ReentrantLock在面试里出现的频率极高但你仔细琢磨一下面试官其实很少直接问“ReentrantLock怎么用”这种使用层面的话因为API太简单了lock和unlock一调谁都会。真正拉开差距的是对它背后机制的理解深度。我大概梳理了一下ReentrantLock相关的面试问题基本集中在这么几个方向锁的基础概念可重入是什么意思跟不可重入锁比有什么好处公平性设计公平锁和非公平锁的差异为什么非公平锁性能更好AQS底层获取锁失败后线程去了哪里怎么恢复state字段的作用与synchronized对比既然synchronized这么好用还免费为什么要用ReentrantLock扩展能力响应中断、超时获取锁、多个Condition队列。这一连串问题背后考的是你对并发核心机制的理解而不是单纯背八股。你得能说清楚“线程拿不到锁时到底是阻塞还是自旋”“公平锁为什么需要检查队列前驱”“可重入计数会不会溢出”这种细节。所以这篇我决定换个思路不讲那些人人都有的结论而是跟着源码把整个过程走一遍把关键节点掰开揉碎了讲。2. 锁的骨架AQS与ReentrantLock的关系2.1 为什么ReentrantLock要基于AQS实现AQS全称AbstractQueuedSynchronizer翻译过来是抽象队列同步器。它不是一个具体的锁而是一个用来构建锁和同步器的框架。ReentrantLock只是AQS的一个应用场景同门的ReentrantReadWriteLock、Semaphore、CountDownLatch全都是靠AQS来实现核心同步逻辑的。你去看ReentrantLock的源码会发现它内部有三个关键类Sync、FairSync、NonfairSync。其中Sync是个抽象类继承了AQSFairSync和NonfairSync分别代表公平锁和非公平锁都继承自Sync。也就是说ReentrantLock本身没干多少事真正的获取锁、释放锁、排队阻塞逻辑全部是AQS在底层撑着。那为什么要把这么复杂的逻辑抽到AQS里因为锁也好、信号量也好、倒计时器也好它们面临的底层问题其实是一模一样的多线程竞争一个共享状态抢不到的要排队等待状态变更后要唤醒等待者。把这些公共逻辑抽出来新写一个同步器只需要实现几个钩子方法就行了不需要每次从零开始搞等待队列、线程阻塞唤醒这些重活。这种设计思路在工程上很常见本质上是模板方法模式的一种实践。AQS把流程定死了你看源码的话会发现acquire方法里先尝试获取同步状态失败就构建节点入队然后在队列里循环等待被中断还要响应这套流程对所有的同步器一视同仁。唯一允许子类自由发挥的是tryAcquire这个钩子方法——公平锁怎么抢、非公平锁怎么抢、信号量怎么减计数都由子类去实现。2.2 公平锁与非公平锁的抉择ReentrantLock的构造函数可以传入一个布尔值true代表公平锁false代表非公平锁默认是非公平锁。很多初学者不理解为什么默认要选非公平的这不就是允许插队吗先看看非公平锁的抢锁逻辑。NonfairSync的tryAcquire方法实现是这样的protected final boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 关键点直接CAS竞争不看队列里有没有人在等 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; }而公平锁FairSync的实现在c 0的时候多了一个判断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; } } // ... 省略可重入逻辑 return false; }差异就在那一行hasQueuedPredecessors。这个方法的语义是当前线程是不是队列中等待时间最长的那个或者队列是不是空的。如果不是说明前面还有人在排队那对不起按顺序来你不能插队。那非公平锁到底哪里插队了想象一个场景锁被线程A持有着线程B和线程C依次来请求锁获取失败后进入了AQS的等待队列。这时候线程A释放了锁线程B被唤醒准备去抢锁。但在线程B真正抢到锁之前一个全新的线程D跑过来调用了lock()方法。在非公平锁下D会直接CAS尝试把state从0改成1如果成功锁就被D抢走了线程B只能继续在队列里等着。这就是插队。表面上看这不公平但为什么还要这么设计核心原因是性能。唤醒一个线程需要线程状态切换和上下文切换的开销被唤醒的线程真正获得CPU时间片还有延迟。与其傻傻等被唤醒的老线程去抢不如让正在活跃运行的新线程先试试反正它马上就要执行临界区代码了。如果让新线程主动让步它大概率会立刻被阻塞白白浪费一次运行机会。实测下来高并发场景下非公平锁的吞吐量通常高于公平锁因为减少了线程的阻塞唤醒次数。当然代价是牺牲了一部分公平性极端情况下某些线程可能长时间得不到锁也就是所谓的饥饿问题。由于线程调度本身无法保证绝对均匀外加非公平模式下插队概率存在少数线程在重负载下可能等很久。但从工程角度看这种牺牲换来的性能收益是值得的。3. 核心机制解析可重入与锁的获取释放3.1 可重入的底层实现ReentrantLock的可重入意思是一个线程可以多次获取同一把锁而不产生死锁。打个比方你进了自家大门进了客厅发现卧室门也锁着但卧室门的钥匙跟大门是同一把直接就能打开。可重入锁就是这么个逻辑同一个线程对同一把锁的重复加锁只是让计数加一不会把自己挡在外面。这个计数就是AQS里的state字段。源码里的Sync有这样一个方法abstract static class Sync extends AbstractQueuedSynchronizer { final 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改成acquires通常就是1同时把当前线程记录为锁的持有者。第二步如果state不为0说明锁已经被占用了这时候检查占用者是不是当前线程如果是直接把state加上acquires。state每加一次就代表当前线程多获取了一层锁。释放锁的逻辑正好相反。tryRelease方法里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; }先看看持有者是不是当前线程不是就直接抛异常。然后把state减去releases释放一次就是减1只有当state减到0的时候才说明所有层级的锁都释放完了这时才真正把持有者线程置空。这里有个细节值得注意为什么state减到0才允许后续线程竞争如果只减到1说明外层还有一些加锁没有对应释放锁还是被当前线程持有的自然不能放别人进来。你仔细品一下这个设计本质上就是把“线程身份 一个可变的计数”结合起来实现了同一个线程对共享资源的多次安全进入。这也解释了为什么可重入锁能避免死锁线程自己在持有锁的状态下调用自己内部需要同锁的方法不需要重新竞争直接走else if分支把计数加上就完事了。3.2 lock()获取锁的完整路径我一直觉得搞懂lock()方法的执行路径是理解整个AQS的关键。咱们从ReentrantLock的入口看起public void lock() { sync.acquire(1); }这里的acquire是AQS里定义的方法非final但默认实现已经规定好了流程public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这一段很短但信息量极大。第一个动作是调用tryAcquire尝试获取同步状态这个方法就是前面讲公平锁非公平锁时分析的那个方法子类各自实现。如果tryAcquire返回true那就说明获取锁成功整个acquire方法直接结束线程继续往下执行。如果返回false说明锁没抢到这时候就进入acquireQueued流程。addWaiter这个动作是把当前线程包装成一个Node节点挂到AQS内部的等待队列尾部。这个队列是什么结构AQS内部维护了一个双向链表每个节点持有前驱引用、后继引用还有thread和waitStatus两个关键字段。这个队列的作用就是给所有没抢到锁的线程排一个先来后到的顺序。节点入队之后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); } }这个循环的逻辑是当前线程对应的节点入队后先看看自己的前驱是不是头节点。如果是说明排到自己了再试一次抢锁注意这里用的是公平的队列顺序即便在非公平锁下已经在队列里的线程也遵守先后顺序。如果抢成功就把自己设为新的头节点。如果前驱不是头节点或者抢锁失败就进入park状态把线程挂起。我在这里给一个容易混淆的点做个澄清非公平锁所谓的“插队”只发生在入队之前也就是第一次tryAcquire的阶段。一旦线程已经入了队列就必须严格遵守队列顺序排在后面的不能越过前面的线程。这个设计背后的原因很简单如果队列内部也允许插队那整个队列就失去意义了等待中的线程可能会无限期被饿死。线程被park挂起之后谁来唤醒它答案是前驱节点。前驱节点在释放锁时会通过unparkSuccessor找到它的后继节点调用LockSupport.unpark唤醒后继线程。被唤醒的线程从parkAndCheckInterrupt返回重新进入自旋循环继续检查自己是不是到了队首。这里还有一条容易忽略的细节如果线程在排队等待期间被interrupt了parkAndCheckInterrupt会返回trueacquireQueued会记下这个中断标记但不会立即中断线程。它的策略是等获取到锁之后在acquire方法里通过selfInterrupt()补上中断。为什么这么绕因为ReentrantLock的lock()方法是不响应中断的线程即使收到中断信号也得先把锁拿到再回到用户的代码里通过Thread.interrupted()之类的方式去感知中断。这种行为跟lockInterruptibly()是截然不同的后面面试题部分再展开。3.3 unlock()释放锁之后发生了什么解锁的代码看起来更简单public void unlock() { sync.release(1); }release同样是AQS的方法它的流程分两段。第一段是tryRelease前面分析过把state减1等state归零时释放持有者标记返回true。第二段就比较有意思了当tryRelease返回true说明锁已经完全释放需要唤醒队列中正在等待的线程。关键在于唤醒哪个线程。AQS的唤醒逻辑不是随便唤醒随便一个而是必须唤醒队首的下一个有效节点。源码是unparkSuccessor它的逻辑是先找到头节点的后继节点如果后继节点状态合法直接LockSupport.unpark它如果后继节点被取消了CANCELLED状态就从队尾往前遍历找第一个有效的节点来唤醒。为什么从队尾往前找而不是从前往后找注释里给出的解释是往后的next指针在某些取消场景下是不可靠的可能在插入新节点时还没来得及设置完整。为了保证一定能找到最合适的唤醒目标采用从尾部向前的扫描方式更安全。还有一个细节很多人在面试时会被问到unlock的时候当前持有锁的线程需要先把head节点置空吗答案是不需要。释放锁的线程只需要修改state和exclusiveOwnerThread下一个线程抢到锁之后会在acquireQueued里把自己设为新的head同时清掉老head的next引用等着被GC回收。如果你在自己写代码的时候观察过jstack输出会发现被阻塞在ReentrantLock上的线程处于WAITING状态线程栈里会有一句parking to wait for 某个对象引用这个对象其实就是锁底层关联的AQS队列里的某个Node。借助这个特征排查死锁时能很快锁定线程到底阻塞在哪把锁上。4. ReentrantLock与synchronized的正面较量4.1 功能维度全面对比synchronized是Java语言内置的关键字从JDK 5开始ReentrantLock在功能上跟它拉开了差距。咱们用表格直接看差异对比项ReentrantLocksynchronized公平性控制构造时指定公平或非公平非公平中断响应支持lockInterruptibly等待可被中断等待中不可响应中断超时获取支持tryLock(timeout, unit)不支持多个等待条件可创建多个Condition只能配合一个wait/notify锁释放方式必须手动unlock通常在finally中由JVM自动释放不会忘记底层机制基于AQS线程阻塞由LockSupport.park基于JVM字节码指令monitorenter/monitorexit锁升级由JVM管理synchronized有一个巨大的优点不用手动释放异常退出时JVM会自动释放锁不会出现死锁。ReentrantLock虽然功能多但对使用者的纪律性要求更高忘记unlock就会造成其他线程永久阻塞这是它最被诟病的一点。另一个常见的误区是“synchronized性能比ReentrantLock差”。这个说法已经过时了。从JDK 6开始synchronized引入了偏向锁、轻量级锁、重量级锁的升级机制在低竞争场景下几乎可以做到无锁或轻量级加锁。而在高竞争场景下两者的性能都是靠阻塞唤醒机制兜底ReentrantLock因为有非公平模式减少唤醒次数往往略有优势。但这不是我们选型的主要依据。4.2 实际场景中怎么选型我个人的经验是能用synchronized就优先用synchronized因为它简单、不容易出错、可读性好。比如一个方法里就几行代码需要同步直接加synchronized最干净。什么时候必须换ReentrantLock我有三个典型场景。第一个场景是需要公平锁的时候。synchronized是完全非公平的如果业务要求线程获取锁的顺序必须跟请求顺序一致那只能选ReentrantLock的公平模式。不过要泼一盆冷水公平锁的吞吐量偏低实际项目中除非有明确需求否则默认用非公平锁就够了。第二个场景是必须响应中断。比如一个线程在等锁时被用户取消任务了我们希望它能立即感知中断并退出而不是傻傻等到锁到手。这时候lockInterruptibly就派上用场了。第三个场景是需要多个等待队列的场景。比如实现一个容量有限的缓冲队列一个Condition负责“不满”时通知生产者继续放数据另一个Condition负责“不空”时通知消费者来取数据。synchronized只有一个wait/notify想精确唤醒特定类型的线程是做不到的ReentrantLock加上多个Condition就能优雅解决。说到底层性能很多人忽略了另一个关键因素synchronized的锁与对象头关联经过锁升级后重量级锁同样会阻塞线程而ReentrantLock的阻塞唤醒依赖AQS和LockSupport。所谓性能谁好谁坏不能脱离竞争强度和临界区大小来讨论。竞态低、临界区短两者都很快竞态高、临界区长两者都会产生上下文切换开销这时候选哪个主要看需要哪些扩展功能。5. 面试题汇总与回答思路5.1 拿下高分的六个核心问题结合我平时跟同行交流以及自己面试别人的经验把出现频率最高的6个ReentrantLock相关问题整理一下每个问题都给一个答题框架既有结论也有推导过程。第一个问题ReentrantLock是如何实现可重入的这个前面已经详细分析过了。答题思路要抓住“state计数器 持有者线程身份”这两个关键点当前线程获取锁时检查statestate为0说明空闲则CAS设置并记录持有者state不为0则判断持有者是否当前线程是的话state加1。释放时state减1直到归零才真正释放锁。如果再答一下“如果state溢出怎么办即处理Maximum lock count exceeded”就显得更加细节。第二个问题公平锁与非公平锁的区别先说结论公平锁按照线程请求锁的顺序获取锁非公平锁允许新来的线程直接竞争锁。实现差异就在tryAcquire里多了hasQueuedPredecessors的判断。然后要补充为什么非公平锁性能更好就是因为减少线程唤醒的时间窗口让活跃线程有机会直接拿到锁。如果可以进一步说明队列内的线程依然按顺序获取锁插队只发生在入队前。第三个问题获取锁失败的线程接下来干什么这个问题我觉得最能区分候选人的水平。标准回答是tryAcquire失败之后线程被包装成Node加入AQS的CLH变体双向队列尾部然后在acquireQueued里自旋检查自己是否到队首到了就尝试获取锁没到就通过LockSupport.park挂起。被唤醒后重新进入循环。注意这里要提一句自旋不是死循环空转到了队首才会再次抢锁并且在抢锁失败但是条件不满足时会通过LockSupport挂起不会无畏消耗CPU。第四个问题lock()和lockInterruptibly()有什么区别lock()不响应中断即使线程被标记为interrupt状态它也得等获取到锁之后才处理中断。lockInterruptibly()则只要检测到中断立刻抛出InterruptedException并退出。如果在等待锁的过程中调用了thread.interrupt()使用lock()的线程会继续等锁而使用lockInterruptibly()的线程会直接放弃等锁。这个区别在实现取消任务的场景里很关键。第五个问题synchronized和ReentrantLock怎么选现在这个问题的标准答案已经不是“ReentrantLock性能好”了。要先说synchronized的优点简单、自动释放、锁升级机制让它低竞争场景下很高效。然后说ReentrantLock的优势公平锁、可中断、超时、多个Condition。最后给结论不需要高级特性时默认选synchronized需要公平性或中断响应等能力时用ReentrantLock。第六个问题AQS的state为什么用int不用long这个算是偏冷门的问题但真有人问。答案是int在绝大多数场景下够用了。以ReentrantLock为例state表示重入次数你可以想一想同一个线程把同一把锁重入几十亿次这在工程上根本不可能发生。同时int类型的CAS操作在32位和64位JVM上都有高效的原生支持性能更好。用long做CAS虽然在64位系统上开销没那么大但没必要。同样的道理也适用于Semaphore和CountDownLatch的计数。如果非要扛一下极端情况下确实可能溢出但源码里已经有了溢出保护重入次数达到Integer.MAX_VALUE后会抛Error这在正常程序里永远不会触发。5.2 面试中的加分表达回答ReentrantLock相关问题时除了把结论说对我更建议你顺手带上一些源码依据和细节面试官会明显觉得你是真读过源码而不是背了面试题。比如说到非公平锁的插队行为你可以补一句“非公平锁的插队不是无限插队的一旦线程已经进入CLH队列等待就必须严格按照队列顺序获取锁”。这句补充直接展示了你是真的懂队列机制。又比如说到可重入的时候可以延伸一句“ReentrantLock支持的重入是有限度的state是int类型重入次数超过Integer.MAX_VALUE会抛出Error”。这种边角细节杀伤力很大。再说一个容易翻车的地方如果你在面试里提到“ReentrantLock比synchronized性能更好”一定要解释清楚是在什么前提下。锁竞争比较高、临界区比较长、非公平模式下的吞吐量更高。否则面试官追问起来很容易露馅。高性能并发场景用非公平锁的主要收益就是减少不必要的阻塞唤醒开销。6. 实操中的避坑与排查经验6.1 最容易踩的三个坑第一个坑也是最经典的忘记在finally里释放锁。synchronized是自动释放的很多人习惯了这个便利之后转用ReentrantLock就容易漏写unlock。一旦临界区里抛了异常锁永远不会释放其他等锁的线程全部卡死。最终表现就是服务假死CPU不高但请求全部超时。正确写法牢记一个原则lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }要注意的是lock()必须放在try块外面如果lock()本身在try块里面一旦获取锁抛异常finally里还是会执行unlock这就会对一把没拿到的锁执行释放从而抛出IllegalMonitorStateException。这个顺序很多人没注意但确实是个隐藏风险。第二个坑锁的粒度太大。有些同学图简单直接在一个方法上加ReentrantLock整个方法体都是临界区。如果方法内部有网络请求、磁盘IO这类耗时操作锁会被占住很久并发性能直接打回串行。正确做法是尽量缩小临界区只把真正需要互斥的那几行代码包进去。第三个坑是锁顺序不一致导致死锁。比如线程A持有锁1想拿锁2线程B持有锁2想拿锁1两边就僵住了。ReentrantLock比synchronized多了一个辅助排查的能力lockInterruptibly()可以避免死等或者用tryLock设置超时拿不到锁就放弃并回滚。但根本解法还是在自己的代码设计层面保证加锁顺序一致。6.2 怎么定位线上锁问题线上如果出现了锁相关的问题比如“某接口突然大量超时”需要快速判断是不是卡在锁上了。我的排查路径一般是这样。先看线程状态。用jstack或者本地IDE的线程快照功能抓一份线程栈。被阻塞在ReentrantLock上的线程状态是WAITING线程栈里通常会看到类似这行- parking to wait for 0x00000000e3c1a6d8 (a java.util.concurrent.locks.ReentrantLock)那串十六进制地址就是锁对象本身。然后看线程栈上的调用链定位到业务代码里哪一行调用了lock()。多抓几次快照如果发现某个线程每次都卡在同一把锁上基本可以断定这把锁被某个线程长时间持有或根本没释放。还有一种情况是线程明明没有竞争锁却一直不释放。这时候要看代码里是不是有未捕获的异常导致unlock没执行到。铁律是unlock放finally里你要是用ReentrantLock却没用try/finally那真出了线上事故别怪我没提醒你。顺带说一个调优时常用的查看方式如果想知道当前有多少线程在等待某把锁可以调用getQueuedThreads()或者getQueueLength()这两个方法虽然返回的信息有限但在做压测时写个诊断接口观察等待队列长度和锁竞争情况是非常直观的手段。它们不是给业务代码做判断用的因为并发场景下这些数据瞬时波动很大只能当作监控分析的参考。6.3 配合Condition实现精准唤醒Condition是ReentrantLock最有价值的扩展能力之一。通过newCondition()可以创建多个等待队列每个Condition独立管理自己的等待线程集合。用代码来演示一个典型场景——容量有限的缓冲区。生产者和消费者共用同一把锁但分别await在不同的Condition上ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition(); public void put(Object item) throws InterruptedException { lock.lock(); try { while (queue.size() capacity) { notFull.await(); // 队列满了生产者等待 } queue.add(item); notEmpty.signal(); // 通知有货了唤醒消费者 } finally { lock.unlock(); } } public Object take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 队列空了消费者等待 } Object item queue.remove(); notFull.signal(); // 通知有空位唤醒生产者 return item; } finally { lock.unlock(); } }注意这里while循环而不是if判断条件这是为了避免虚假唤醒。线程从await返回后条件可能还是不被满足比如多个消费者同时被唤醒但只有一个元素可取所以必须重新检查。用多个Condition的好处是精细控制唤醒方向生产者唤醒消费者消费者唤醒生产者互不干扰。你要是用synchronized的wait/notify就只有一个等待集notifyAll会把所有等待线程全部唤醒然后让它们自己竞争条件浪费一堆唤醒机会。6.4 关于中断响应你得多留个心眼阻塞在ReentrantLock上的线程想让它停下来得看你用哪个方法获取锁。如果用的是lock()线程在等待锁时对中断是“屏蔽”的interrupt只会把线程的中断标记置为true线程拿到锁之后要继续执行直到代码里自己去检查Thread.isInterrupted()才能感知。如果用的是lockInterruptibly()线程一旦被interrupt会立即抛出InterruptedException连锁都不等了。tryLock(long timeout, TimeUnit unit)也有中断响应能力。实际项目中如果你需要实现“用户在界面上取消一个正在排队等锁的任务”那lockInterruptibly是最合适的方案之一。但要注意捕获InterruptedException后通常要重新设置中断标记或者把异常往上抛别把中断状态给吞了。吞掉中断状态是并发编程里很不好的习惯。最后再补充一点每次讲ReentrantLock我都发现大家容易陷入“背结论不追源码”的误区。其实它整个机制的核心就是那把看不见的state数字再加上一个双向队列和LockSupport的挂起唤醒。你把这三点串起来想遇到任何相关的面试题都能从底层逻辑推导出答案而不是靠记忆堆砌。如果你手头正在准备面试我建议你亲自把ReentrantLock和AQS的源码读一遍关键方法打断点跑一个两个线程竞争锁的用例你会发现很多概念一下子就通了。后面有时间我再写一篇专门讲AQS源码里Condition与CLH队列的深度协同那部分内容比本篇还要绕值得单独出一篇。