深入AQS:ReentrantLock灵活锁机制的设计原理与源码解析 锁是并发编程里永远绕不开的话题。不管是新手还是老手只要写过共享资源的并发访问就一定跟synchronized打过交道而在某个阶段你多半也会遇到ReentrantLock。很多人在第一次接触ReentrantLock时会有一种感觉这东西真的灵活——能中断、能限时等待、能公平排队、还能配合条件变量精确唤醒但它到底怎么做到的为什么用它可以做到这么多synchronized做不了的事答案就藏在AbstractQueuedSynchronizerAQS里。AQS 是 Java 并发包中所有锁和同步器的基础设施ReentrantLock的公平、非公平、可重入、可中断、可超时、条件等待等能力本质上全部是 AQS 提供的机制。这篇文章就来把它彻底剥开从设计骨架到源码细节讲清楚ReentrantLock那些令人羡慕的特性是怎么一步步落地的也顺便把我这些年实际使用中踩过的坑和总结出来的排查技巧一并分享出来。如果你是刚接触并发的初学者建议先把synchronized的基本用法和volatile的内存语义弄清楚再来看如果你已经用过一段时间ReentrantLock那这篇文章正好帮你把底层的“为什么”补全。1. 锁的演进与 ReentrantLock 的诞生背景要理解 AQS 为什么是现在这个长相得先回到它出现之前的年代。Java 最初只有synchronized和volatile两员大将synchronized在大多数场景下够用但它的能力边界很明显不能响应中断、不能设置超时、等待锁期间不能被外部唤醒、获取锁的顺序完全不由开发者控制。这些问题在高并发场景下会放大成实实在在的痛点。举个常见的例子一个异步任务池里线程在等待锁时如果被上层应用要求停止synchronized是没办法做到“你让我停我就赶紧退出等待”的只能等锁到手再处理取消信号可能等到的就是漫长的阻塞。另一个例子两个线程加锁顺序不一致导致死锁如果是synchronized你没有任何手段在上层干预只能重启应用但如果锁支持限时等待线程在拿不到锁时可以主动放弃死锁就在超时机制下自动解开了。JUCjava.util.concurrent包的核心设计师在构建并发工具时选择了一条和synchronized完全不同的道路先抽象出一个通用的同步状态管理框架再基于它派生各类具体的同步器。这个框架就是 AQS。AQS 用volatile int state表示同步状态用内置的 FIFO 等待队列管理线程排队再通过模板方法模式固定算法骨架、开放若干抽象方法给子类定制具体语义这样一把锁、一个信号量、一个倒计时门闩就只是不同子类对同一套底层机制的差异化实现。ReentrantLock是这套框架最典型、也最适合作为学习切入点的实现。它的名字已经概括了核心特点re-entrant可重入。同一线程在已经持有锁的情况下再次调用lock()不会被自己阻塞对应的state会累加计数每次unlock()递减直到归零锁才真正释放给其他线程。这个“持有计数”的思路让锁具备了“线程所有权”的概念也顺带区分开了“公平排队”和“插队竞争”两种获取策略。回到你可能会问的问题既然synchronized后来也实现了可重入、锁升级等能力为什么我们还需要ReentrantLock答案是两者在 API 层面和调度语义上的差距依然很大特别是中断响应、限时获取、公平排队和条件变量这四件事synchronized至今没有原生支持。而这些能力全部来自 AQS 的骨架设计接下来我们就进到源码层面逐个击破。2. AQS 的结构骨架状态、队列与模板方法AQS 之所以能被称作“框架”是因为它把并发同步中最通用的部分提前做好了留出来的只是几块“拼图”让子类决定。2.1 一个 int 搞定所有同步状态AQS 内部的核心字段只有一个private volatile int state;这个state的语义完全由子类定义。对ReentrantLock来说state 0表示锁未被持有state 1表示被某线程持有且值代表重入次数。对Semaphore来说state代表剩余的许可证数量对CountDownLatch来说state代表还差多少个事件才能放行。为什么用一个int就能通吃因为同步器要回答的核心问题就一个当前线程能不能通过这个关口。能通过就继续执行不能通过就排队。至于“通过”的规则交给子类实现。volatile保证了state的可见性而修改state的操作都围绕 CAS 展开AQS 提供了compareAndSetState来保证无锁更新的一致性protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }你可能好奇为什么不用synchronized保护state的修改因为 AQS 本身研究的对象就是如何实现锁如果用锁去保护锁那这套抽象就失去意义了。CAS 是无阻塞的冲突时由调用方决定重试还是失败这正好是“非阻塞同步”的精髓。2.2 CLH 队列的改造从“旋转”到“阻塞”AQS 里还有一个每个 Java 并发工程师都该认识的数据结构同步等待队列。它本质上是 CLHCraig, Landin, Hagersten锁队列的一个变体。传统 CLH 队列是“自旋”的线程在队列里不断轮询前驱节点的状态直到前驱释放锁。自旋在临界区极小、竞争极低的场景下效果很好但一旦临界区耗时较长CPU 就会被白白烧掉。AQS 对 CLH 做了关键改造线程不再自旋等待前驱而是先尝试自旋有限次数某些场景拿不到锁就把自己挂起LockSupport.park前驱在释放锁时负责唤醒后继LockSupport.unpark。队列节点是Node关键字段有waitStatus当前节点的等待状态取值有CANCELLED1、SIGNAL-1、CONDITION-2、PROPAGATE-3、0初始态prev/next双向链表指针thread当前节点代表的线程这里有一个容易被忽略的细节头节点是一个哨兵。AQS 队列的头节点不存储真实等待线程它表示“当前正在持有锁的线程”或“刚释放锁准备让位的线程”。每个新来的线程入队时会追加到队尾并把自己的前驱节点的waitStatus设为SIGNAL意思是“我排在后面前驱释放时记得叫我”。这种“状态接力”的设计保证了unlock时锁的释放动作只需要唤醒一个直接后继而不是广播给所有人。2.3 模板方法模式固定骨架开放钩子AQS 的设计模式相当优雅它是“模板方法模式”在并发框架里的教科书级应用。AQS 把获取锁和释放锁的流程骨架固化好但把“具体规则”留给子类tryAcquire(int arg)尝试独占获取成功返回truetryRelease(int arg)尝试独占释放成功返回truetryAcquireShared(int arg)尝试共享获取返回剩余可用资源数或负值表示失败tryReleaseShared(int arg)尝试共享释放isHeldExclusively()当前线程是否独占持有ReentrantLock只需要咬住tryAcquire和tryRelease两个方法其余交给 AQS 的通用算法。这就是为什么ReentrantLock的源码看起来“瘦”核心逻辑在 AQS 里已经写死ReentrantLock只是提供了不同的tryAcquire策略而已。从这个角度看AQS 就像是一台标准化程度很高的加工机床ReentrantLock、Semaphore、CountDownLatch这些“工件”虽然形状不同但都在这台机床上加工出来。你要深入理解其中任何一个首先要把机床的运行原理吃透。3. 可重入与互斥的底层实现这一节我们逐行拆ReentrantLock获取锁和释放锁的完整路径。很多人看源码时被acquire、acquireQueued、addWaiter这些方法绕晕我把整条链子串起来你顺着走一遍就清楚了。3.1 lock() 到 tryAcquire一次获取尝试的完整链路ReentrantLock.lock()默认走非公平策略入口在内部类NonfairSyncfinal void lock() { // 先直接 CAS 抢一次这就是“非公平”的插队入口 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }第一次CAS成功说明锁是空闲的当前线程直接成为持有者连队列都不用进。这一下就是非公平锁吞吐量高的原因之一——当锁刚被释放时正好有线程在这里抢它就是能比排队线程先拿到。CAS 失败的线程进入acquire(1)这是 AQS 提供的模板方法public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这里tryAcquire是ReentrantLock自己实现的核心钩子方法。非公平锁版本的逻辑是protected final boolean tryAcquire(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; }两个关键点都集中在这个方法里非公平体现在c 0分支它不关心等待队列里有没有“老人”在排队直接尝试 CAS 抢锁。可重入体现在current getExclusiveOwnerThread()分支如果当前线程就是锁的持有者state直接加上acquires通常就是1不需要 CAS也不需要排队。tryAcquire失败后线程会进入排队流程。addWaiter负责包装节点并追加到队列尾部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失败并发下尾部被其他线程抢先更新才走enq自旋重试。这其实是一种轻量级优化避免大部分情况下直接进入复杂逻辑。3.2 acquireQueued排队线程的阻塞循环入队之后线程不会立刻阻塞而是进入acquireQueuedfinal 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); } }这段代码是在一个死循环里反复执行两个动作先看前驱是不是头节点如果是再尝试获取一次锁如果获取失败检查自己是不是应该挂起。shouldParkAfterFailedAcquire的逻辑很精妙。它检查前驱节点的waitStatus如果前驱是SIGNAL说明前驱已经承诺“我释放时会唤醒你”当前线程可以放心阻塞了返回true。如果前驱waitStatus 0即CANCELLED说明前驱已经取消排队获取锁当前线程要往前跳越过所有被取消的节点返回false继续循环。如果前驱状态是 0 或其他说明前驱还没设置唤醒承诺当前线程通过 CAS 把前驱的状态改成SIGNAL返回false继续循环。所以一个真实的挂起过程是这样的入队后第一次循环前驱不是头节点或者尝试获取锁失败此时把前驱设为SIGNAL回到循环第二次循环前驱已经是SIGNAL当前线程执行LockSupport.park真正挂起。这个“两阶段进入阻塞”的设计是为了尽量少地让线程进入挂起状态避免无谓的上下文切换。顺带一提parkAndCheckInterrupt返回当前线程是否被中断过但注意在synchronized里中断一个等待锁的线程它会直接抛出InterruptedException而在lock()路径下中断不会让线程退出只会记录interrupted true在acquire返回后由selfInterrupt补设中断标志。这就是“不可中断获取”的语义lock()的中断标志会保留但不会打断等待锁的过程。3.3 unlock 流程release 与 unpark 的配合释放锁的路径相对简单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实现的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减 1只有减到 0 才真正释放所有权。这里的“只有归零才算释放”正是可重入语义在释放侧的映射。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; // 从 tail 往前找找到最接近 head 且没被取消的节点 for (Node t tail; t ! null t ! node; t t.prev) if (t.waitStatus 0) s t; } if (s ! null) LockSupport.unpark(s.thread); }为什么next可能为null或者指向已取消节点因为入队操作是先设置prev再 CAS 设置tail中间存在窗口期后继的prev已经链上但next还没补全还有取消节点会把自己从链中断开。所以从tail往前反查是最安全的找后继方式。这个细节在并发队列实现里很常见——正向遍历可能漏反向遍历不会。3.4 从一次加锁过程反推整体设计逻辑把整个链路串起来看ReentrantLock.lock()的完整流程可以浓缩成一句话先尝试轻量级的 CAS 抢锁抢不到就进队列入队后先自旋试探几次不行就挂起等前驱释放时唤醒唤醒后再次尝试抢锁周而复始。这个设计兼顾了三种场景锁空闲时的极速获取一次 CAS锁竞争不激烈时的短等待自旋几次就能拿到锁竞争激烈时的长等待挂起避免浪费 CPU。没有设计成“一上来就阻塞”是因为线程创建和挂起唤醒的成本非常高尤其是在临界区极短的场景下自旋往往是更优选择。我从实际测试里得到的经验是如果临界区平均执行时间在几十微秒以内自旋等待比挂起唤醒的吞吐量能高一个数量级如果临界区执行时间达到毫秒级自旋就变成了 CPU 的浪费。AQS 采用“先自旋再挂起”的折中策略是为了在这两种场景之间取得平衡。4. 公平锁与非公平锁的博弈ReentrantLock最有意思的设计之一就是同一个锁可以有两种获取策略。你只需要在构造时传一个布尔值ReentrantLock fairLock new ReentrantLock(true); ReentrantLock unfairLock new ReentrantLock(false); // 默认两者的底层差别其实非常小小到只有tryAcquire一个方法里的几行代码不同。4.1 公平锁的 tryAcquire先看队列再决定公平锁版本的tryAcquire是在获取锁之前先检查同步队列里有没有人在排队protected final boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 关键差异hasQueuedPredecessors 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()判断的是等待队列里是否已经有线程比当前线程更早到达。如果有返回true当前线程就不能抢锁老老实实去排队。这个方法的实现有非常多的细节用到了head ! tail的判断、tail是否为null等等。从语义上讲它就是“公平排队”的核心保障。4.2 非公平锁为什么吞吐量更高非公平锁允许新到的线程在锁空闲时直接插队哪怕队列里有线程在等着。这听上去“不公平”但它有两个实打实的好处第一避免了唤醒开销。队列里的线程被唤醒需要上下文切换而新来的线程本来就在 CPU 上运行如果让后者直接获取锁就省掉了一次“挂起-唤醒”的完整切换。排队线程的唤醒虽然被延迟了但总体上系统做了更少的无用功。第二减少不必要的调度延迟。假设锁释放的频率很高、但每次持有时间极短如果强制执行公平排队每个线程获取锁都要经历“挂起-等待被唤醒”的完整路径锁的实际吞吐会显著下降。非公平锁的“插队”相当于把一部分获取动作从“异步唤醒路径”转移到了“同步执行路径”延迟更低。但非公平锁也有明显的代价极端情况下后来的线程可能连续抢锁导致队列里的线程长时间得不到锁出现“饥饿”。Java 的ReentrantLock非公平实现还保留了排队线程的唤醒机制所以理论上不会完全饿死只是等待时间会变长。我做过一个简单的压测同样是 8 线程竞争、临界区做 1000 次累加操作非公平锁的吞吐量大概比公平锁高 15% 到 30%。随着竞争加剧和临界区缩短这个差距还会拉大。所以在高吞吐场景下非公平锁几乎是默认选择只有当你明确要求“按到达顺序执行”时比如某些任务调度场景才需要切到公平锁。4.3 选择建议与注意事项在我的项目经验里公平锁的使用场景非常窄。大多数业务场景只需要保证最终一致性和资源互斥不要求严格按请求到达顺序执行。少数场景比如限流器、任务分配器如果希望所有等待线程公平分享资源会优先考虑Semaphore或ReentrantLock(true)。还有一个容易踩的坑公平锁不保证“线程调度公平”。它只保证“队列内部排队顺序”公平但如果某个线程频繁在锁释放的间隙抢到锁因为它是下一个排队者那它在宏观上仍然会比后面的线程执行得更多。这个概念要分清不要以为公平锁就万事大吉。5. 中断、超时与条件变量的“灵活”底牌前面讲的可重入和公平策略只是ReentrantLock灵活性的一个维度。真正让它和synchronized拉开差距的是中断响应、超时获取、条件变量这三张牌。5.1 响应中断lockInterruptibly 的实现路径lockInterruptibly()允许线程在等待锁的过程中响应中断信号一旦线程被 interrupt会立刻抛出InterruptedException从等待中醒来。这在实现“可取消任务”时非常关键。它的核心路径是AQS.acquireInterruptiblypublic final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }doAcquireInterruptibly的结构和acquireQueued基本一致唯一区别是parkAndCheckInterrupt检测到中断后不记录标志位而是直接抛出InterruptedException。这种情况下的语义是“我现在不想等了立刻退出”。对应的使用场景比如分布式任务中线程等待锁时收到 shutdown 信号应该快速释放等待并进入清理流程。如果用synchronized你无法在等待锁期间响应中断用lockInterruptibly就可以。需要注意区分三种 API 的中断语义API等待锁期间被中断的表现适合场景lock()记录中断标志继续等待要求必须拿到锁lockInterruptibly()立刻抛异常退出可取消任务tryLock(time, unit)如果超时或中断立刻退出有界等待避免死锁5.2 限时等待tryLock 的妙用tryLock(3, TimeUnit.SECONDS)是最常用的“避免死锁”手段。它的实现走tryAcquireNanospublic final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); }doAcquireNanos在循环获取锁的过程中加入了超时判断如果剩余时间小于一定阈值比如 1000 纳秒事实上是spinForTimeoutThreshold就不再挂起而是直接自旋否则用LockSupport.parkNanos精确挂起剩余时长。这里有一个实现细节很有意思AQS 不是重新计算“绝对截止时间”来判断超时而是在每次循环里用System.nanoTime()实时对比。System.nanoTime()适合计算时间间隔不受系统时间调整影响这个选择是对的。我习惯用tryLock的方式解决“多把锁交叉获取”导致的死锁问题。比如两个线程各自持有一把锁同时需要另一把锁如果都用lock()就会互相等待改用tryLock加超时后拿不到就释放已有的锁、退避重试死锁变成可恢复的竞争。5.3 条件变量Condition 的等待与通知机制如果说前面几张牌是“锁本身”的灵活性那Condition就是一把锁之外的另一套信令系统。synchronized只有wait/notify/notifyAll一套等待集合而ReentrantLock可以创建多个Condition每个Condition有自己独立的等待队列可以精准地唤醒某一类等待线程。创建方式很简单ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition();典型的“生产者-消费者”模型里生产者等待“不满”消费者等待“不空”两种等待被打散到两个独立的条件队列中。生产者只需要唤醒“不空”这个条件上的消费者不会误唤醒其他生产者减少了无效调度。Condition.await()背后是 AQS 的ConditionObject它内部维护着一个单向的条件等待队列。await的实现逻辑把当前线程封装成NodewaitStatus设为CONDITION释放锁唤醒后继线程挂起当前线程直到被signal唤醒唤醒后重新加入同步队列竞争锁获取锁后从await返回这个过程我建议你重点关注“释放锁”这一步。很多写Condition代码的人踩过同一个坑在持有锁的情况下调await结果发现别的线程也进不了临界区。原因就是没理解await会先释放锁再挂起。如果你的临界区内需要“放下锁、等待某个外部条件再重新拿锁继续”那await就对了如果只是短期阻塞await反而引入更多上下文切换开销。signal的实现也有讲究它只是把条件队列头部的节点转移到同步队列的尾部并把它前驱的waitStatus设为SIGNAL。真正的唤醒动作发生在锁释放阶段。也就是说signal不等于“立刻唤醒”它只是“安排一次未来的唤醒”。这个设计避免了信号在锁持有期间丢失。5.4 Condition 使用的常见误区我在评审代码时见过不少Condition用错的例子其中最典型的三个第一忘记在循环里等待。await可能因为虚假唤醒spurious wakeup或竞争条件而提前返回必须把条件判断放进while循环。正确写法是while (queue.size() capacity) { notFull.await(); }第二signal误用成signalAll。当等待线程的唤醒条件互斥时比如同一个条件队列里有多种不同类型等待者signal可能唤醒一个不满足条件的线程导致它重新阻塞。这时候要评估是使用多个Condition还是直接用signalAll。第三await和signal没有在同一个锁保护下调用。Condition的使用要求await、signal、signalAll都在持有对应锁的代码块内调用否则会抛IllegalMonitorStateException和synchronized块内不能调wait是同一个道理。6. 实战中的踩坑记录与排查建议概念讲得再细不如实战里遇到的几个问题来得深刻。这里把我在真实项目中遇到的一批有代表性的 AQS 相关问题做一个整理希望能帮你少走弯路。6.1 案例一重入次数溢出导致锁“神秘”释放有次排查一个偶发问题现象是某个线程明明还在执行临界区代码其他线程却能拿到同一把锁。后来定位到原因锁被重入了非常多次每次重入state 1一旦重入次数超过Integer.MAX_VALUEstate变成负数tryRelease时c 0的判断就会提前命中因为负数减到0时恰好匹配锁被错误释放。这类问题在递归调用加锁的代码里最容易出现。递归层数很深时每次递归都获取同一把锁重入计数可能无限上涨。AQS 源码里其实有防护逻辑nextc 0会抛出Error但在state已经很大的场景下Error本身也可能被业务代码吞掉导致后续行为异常。排查建议在tryRelease走到c 0时多看一眼持锁线程是否和释放线程一致加锁和释放必须严格配对且在 finally 块里释放可以避免大部分计数错乱。6.2 案例二公平锁下“线程饿死”的假象有同事反馈某个公平锁场景下队列中的线程长时间拿不到锁。分析后发现不是队列线程被饿死而是业务代码里“获取锁”和“释放锁”之间执行时间极长下游接口超时重试导致同一个线程反复入队出队。队列里的等待者看起来长期不动实际上是锁本身被一个慢线程长时间占用。排查方法很简单抓一次线程转储看锁的持有线程在干什么。如果持有线程在等待网络响应或数据库查询那问题不在锁而在临界区耗时。锁的公平性只决定“谁先拿”不决定“拿多久”。6.3 案例三中断信号丢失的连锁故障有个任务调度系统要求“任务取消时立刻释放锁等待”代码用了lock()而不是lockInterruptibly()。结果任务取消后线程依然阻塞在锁上直到锁被释放才执行后置清理逻辑导致资源迟迟不释放引发连锁超时。lock()的中断语义是“被中断但继续等待”中断标志会在获取锁后重新设置但线程本身不会提前退出。如果你的代码里存在“取消后需要快速响应”的场景必须用lockInterruptibly()或带超时的tryLock并且处理时要重新检查中断标志try { lock.lockInterruptibly(); // 业务逻辑 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 重设中断标志 // 清理并退出 } finally { lock.unlock(); }现中断处理有一个我建议大家遵守的原则只要捕获了InterruptedException在退出前都应该重新设置线程的中断标志否则外层代码会失去“线程被中断”这个信息导致更上层的协调逻辑无法感知。6.4 排查 AQS 问题的高效路径如果你遇到锁相关的死锁、性能下降、任务卡死我建议按这个顺序排查先用jstack抓取线程转储看等待锁的线程在哪个park点阻塞找到持有锁的线程确认它卡在哪里。如果持锁线程也在park说明它还在等待另一把锁这就是典型的锁顺序死锁检查是否存在Condition等待确认signal是否有可能在await之前发出检查锁的获取方式lock()、lockInterruptibly()、tryLock的语义差异是否被误用如果怀疑性能问题直接看线程转储里RUNNABLE状态线程的调用栈。如果大量线程在 CAS 自旋acquireQueued中说明竞争激烈可能需要减小临界区或改用其他同步策略排查时要学会看线程转储里park的含义。LockSupport.park挂起时线程状态是WAITING (parking)如果很多线程堆积在同一个park点且队列长度持续增长那大概率是锁的释放频率太低或者持锁时间太长。7. 性能观察与使用心得最后聊一点关于 AQS 性能的观察和最真实的使用心得这部分不是源码分析而是来自压测和实践的经验。7.1 自旋阈值与临界区时长的关系AQS 不是把所有请求直接挂起而是先让线程在acquireQueued循环里自旋数次由shouldParkAfterFailedAcquire控制节奏当waitStatus已经变为SIGNAL后才真正挂起。这个“自旋转挂起”的切换点是性能的关键。如果临界区非常短几微秒自旋等待的成本远低于线程挂起和唤醒的上下文切换成本高吞吐场景下收益显著。如果临界区很长毫秒级以上自旋只是在白白浪费 CPU。我做过一组对比一个临界区耗时100ns的场景非公平锁让 16 个线程并发执行累加吞吐量可以达到公平锁的 2 倍以上而临界区耗时10ms时两者差距几乎消失因为瓶颈变成了临界区本身的执行时间而不是锁的获取方式。这给我们的启发是锁的选型不能脱离临界区耗时单独讨论。如果你的临界区很长与其纠结公平还是非公平不如优先考虑能不能减小临界区范围、用读写锁分离读多写少的场景、或者用无锁结构替代锁。7.2 锁降级与分段思想我在一个高并发统计场景里经历过一次显著的性能提升。最初用一把ReentrantLock保护整个统计缓存吞吐一直上不去。后来改成分段锁把缓存拆成 16 段每段独立一把锁不同线程竞争不同段时互相不阻塞吞吐提升了大约 5 倍。这个思路本质上是在利用“不同维度资源可以被并行访问”的特性。AQS 的state机制让分段锁实现非常干净——每个分段就是一个独立的ReentrantLock互不干扰。你也可以用Striped类来做更细粒度分片但分段数量要和实际并发度匹配分太细反而增加管理和调试成本。7.3 关于 synchronized 与 ReentrantLock 的最终选择现在 JDK 对synchronized做了大量优化偏向锁、轻量级锁、锁升级、锁粗化在很多简单互斥场景下它的性能已经和ReentrantLock相差无几甚至更好因为 JIT 可以对synchronized做更激进的优化。所以我通常的建议是简单互斥、不需要中断、不需要超时优先synchronized代码更简洁需要可重入计数之外的灵活性、需要条件变量、需要限时等待ReentrantLock读多写少ReentrantReadWriteLock或StampedLock只在特殊场景下需要精确控制等待队列ReentrantLock(true)或自定义 AQS 同步器我在实际项目中synchronized使用比例依然很高因为它简单、不容易错、排查成本低。但这并不意味着可以不理解 AQS——恰恰相反只有理解了 AQS你才能真正判断什么时候该用synchronized什么时候该换ReentrantLock遇到问题也能顺着源码把根因找出来。很多并发疑难杂症最后都是靠“会读 AQS 源码”这个能力解决的。顺带提一句如果你要自己实现一个同步器比如限流器、一次性门闩、自定义读写锁AQS 是绕不开的基座。你只需要实现数个钩子方法然后调用acquire、release、acquireShared、releaseShared就能获得一套完整、经过千万级场景验证的队列管理机制。这份家底带来的稳定性远比自己从零写一个锁要可靠得多。所以我个人觉得花时间把 AQS 的源码读透彻是 Java 并发领域性价比最高的投资之一。它不仅能让你更聪明地使用ReentrantLock和其他 JUC 同步器还能让你在看任何基于 AQS 构建的框架时拥有“一眼看穿底层逻辑”的能力。至于ReentrantLock凭什么这么灵活答案你已经看到了它把最复杂的排队、阻塞、唤醒机制全部交给 AQS自己只专注“怎么定义锁的规则”这一件事。这种分工正是它的所有灵活性得以成立的根本原因。