ReentrantLock底层源码解析:AQS队列与CAS锁机制深度剖析

发布时间:2026/10/16 8:17:03

ReentrantLock底层源码解析:AQS队列与CAS锁机制深度剖析 ReentrantLock这个名字搞Java并发的兄弟应该都不陌生。日常写多线程代码synchronized用得多但一旦涉及超时等待、可中断、公平性这些需求ReentrantLock就是绕不开的替代方案。用起来简单lock()和unlock()成对调用就完事可真正想知道它凭什么能实现这些能力光看使用文档是不够的。今天这篇我就直接撕开它的底层从“抢座位”这个生活化的场景切入把ReentrantLock源码和AQS队列的运作机制完整串一遍。不管你是刚开始看并发源码的新手还是已经被AQS折磨过几轮的进阶选手这篇都值得你泡杯茶慢慢读。很多人看源码最大的障碍是被抽象类、模板方法、状态位这些概念绕晕。其实AQS的核心思想放到现实生活里特别简单一堆人抢有限的座位抢到的进去办事没抢到的去排队等候区坐着前面的人办完出来再叫下一个。ReentrantLock的整个源码本质上就是把这个场景用代码精确地实现了。这篇我就带着你把这个场景从“抢”到“排”再到“叫号”的每个环节对照源码看一遍顺便把里面容易踩的坑、容易误解的点统统讲清楚。1. 抢座位只是表象AQS才是ReentrantLock的发动机先明确一个概念ReentrantLock本身并不实现锁的底层机制它的所有能力都委托给一个内部对象——AQS全称AbstractQueuedSynchronizer。AQS是Java并发包里的一个抽象基类很多同步工具都建立在它之上ReentrantLock只是其中一个最典型的应用场景。1.1 为什么需要AQS这样一个“中间层”如果让你设计一把锁你首先想到的是什么大概率是一个标记位比如int state0表示没人持有锁1表示有人持有了。获取锁就是把这个state从0改成1释放锁就是把它从1改回0。这个思路没毛病但马上会遇到几个问题多线程同时修改state怎么保证原子性用synchronized加锁那锁本身还要依赖锁就成死循环了。抢不到锁的线程怎么办直接放弃显得太粗暴原地自旋又太浪费CPU总得有个地方让它们“休息”。后来者是否允许插队这就是公平与非公平的问题。AQS把这些共性问题全部抽象出来用三个核心组件来回答组件作用生活类比state 状态位记录锁的持有状态与重入次数座位上的“有人/无人”标志牌CLH队列变体存放等待获取锁的线程候场区的排队座位CAS操作原子地修改状态位眼疾手快地抢座位ReentrantLock只负责定义“什么是获取成功”和“什么是释放成功”这两个业务规则剩下的排队、阻塞、唤醒这些通用机制全部交给AQS。这就是模板方法模式的经典应用。你读源码时会看到acquire、release这些方法在AQS里已经是final的不可修改而tryAcquire、tryRelease则是抽象方法或者默认抛异常由子类去实现。获取失败后的排队逻辑是固定的但“什么算获取失败”由ReentrantLock自己定义。1.2 ReentrantLock和synchronized的本质差异有人会问既然synchronized能解决绝大多数并发问题为什么还要搞出ReentrantLock这得从synchronized的几个硬伤说起。synchronized的锁获取和释放是由JVM隐式管理的你没法在中途决定“我不等了”也没法给锁加一个等待超时时间。如果持锁线程因为某个外部调用长时间不返回其他线程只能干等着连退路都没有。ReentrantLock则把控制权交给了开发者lockInterruptibly()等待锁的过程中如果线程被interrupt可以立即响应中断并抛出异常。tryLock(timeout, unit)在指定时间内尝试获取锁超时则放弃。支持公平模式让先到达的线程先获取锁避免饥饿。可以在一个方法里加锁在另一个方法里解锁灵活性更高。这些能力背后靠的全是AQS提供的那套队列和状态管理机制。所以与其说你是在学ReentrantLock不如说你是在学AQS。搞懂了AQS队列是怎么入队、怎么阻塞、怎么唤醒的ReentrantLock的源码基本就通了。这也是为什么几乎所有并发源码分析的文章都会把AQS拿出来单独讲的原因——它太重要了也太通用了。2. 先看“抢”这一步CAS快速获取锁的全过程ReentrantLock默认是非公平锁什么叫非公平就是新来的线程可以先尝试插队插队成功就直接拿到锁走人插队失败才老实去排队。这个“插队”操作就是基于CAS实现的。2.1 CAS是什么为什么要用它CAS是Compare-And-Swap的缩写翻译过来就是“比较并交换”。它的逻辑极其朴素如果当前内存值等于预期值就把它更新为新值并且整个过程是原子操作CPU级别保证不会被打断。用代码来表达大概是这个感觉// 伪代码实际由硬件指令保证原子性 boolean compareAndSet(int expect, int update) { if (this.value expect) { this.value update; return true; } return false; }多个线程同时执行compareAndSet(0, 1)时只有一个线程的CAS会成功因为成功之后内存值已经变成1了其他线程看到的预期值0跟实际值1不相等CAS就返回false。这个机制不需要加锁没有线程阻塞和唤醒的开销是高性能并发的基石。放到“抢座位”的场景里CAS就是你瞅准一个空座位用极快的速度坐下去。多个同时行动的人里只会有一个真正坐到座位上其他人要么发现座位上已经有人了要么在坐下的瞬间被弹开。2.2 nonfairTryAcquire源码逐行拆解直接看源码。ReentrantLock的lock()方法走的是这段路径final void lock() { // 第一步尝试CAS插队 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }这段代码的逻辑非常清晰compareAndSetState(0, 1)尝试把state从0变成1。0代表当前锁没人持有1代表持有了。如果成功调用setExclusiveOwnerThread把当前线程记录为锁的持有者。如果失败说明锁已经被别人持有了进入acquire(1)走排队流程。先注意一个容易忽略的细节非公平锁的插队发生在acquire之前。也就是说一个线程在执行到lock()时不管队列里排了多少人它都会先尝试CAS抢一次。抢到了就直接进去了完全无视队列中正苦苦等待的线程。这就是非公平锁“不公平”的最直接体现。再看acquire(1)的内部逻辑public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }tryAcquire在ReentrantLock的NonfairSync实现中最终会调用nonfairTryAcquirefinal boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 状态为0说明锁被释放了再次尝试CAS获取 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { // 当前线程已经是锁的持有者这是重入 int nextc c acquires; if (nextc 0) // overflow throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }这段代码包含了两个关键分支c 0锁已经释放了再尝试CAS抢一次。这里又有一次插队机会已经很说明问题了。current getExclusiveOwnerThread()如果发现锁本来就是当前线程持有的直接给state加上acquires这就是可重入的底层实现。2.3 可重入的奥秘state计数可重入锁的意思就是同一个线程可以多次获取同一把锁而不产生死锁。比如方法A加锁后调用方法BB也加了同一把锁如果是synchronized这种场景是ok的因为JVM会记录持有线程。ReentrantLock也能做到靠的就是state这个变量当计数器。第一次获取锁state从0变成1。同一个线程再次获取state变成2、3、4……每次unlock时释放一次state减1直到state归0锁才算真正被释放其他线程才有机会获取。而计算可重入次数时代码会检查持有锁的线程是不是当前线程。如果不是当前线程即使state是0也不能直接改state必须通过CAS。这个区分很关键它保证了“别人不能帮你释放锁也不能随便增加你的重入计数”。配合代码来看会更直观。unlock()调用release(1)最终走到tryReleaseprotected 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减到0才算释放成功返回值free为truerealease方法才会去唤醒队列中下一个等待的线程。这里有一个非常典型的坑unlock次数必须和lock次数相等。假设你lock了两次却只unlock一次state从2减到1锁根本没释放其他线程还是会被挡住。这种bug在复杂业务里很难排查因为lock和unlock往往不在同一个方法里。我建议无论代码多简单lock之后立刻写unlock的try-finally结构能从根本上减少这类错误。3. 抢不到就去排队AQS队列的入队机制现在已经有两个线程在竞争一个抢到了座位另一个CAS失败接下来要做的事情就是排队等待。这一步涉及AQS里最核心的数据结构——CLH队列变体。3.1 CLH队列不是普通的队列AQS内部维护了一个双向链表每个节点是一个Node对象里面保存着线程引用、等待状态、前驱节点和后继节点。为什么叫“CLH队列变体”原始的CLH锁是自旋锁线程在排队时不断轮询前驱节点的状态。AQS借用了这个链式等待的思路但改成了基于阻塞的等待方式所以叫“变体”。这个队列的设计有几个精妙之处入队操作需要多个线程并发进行所以入队过程本身必须线程安全。AQS用了CAS来保证。队列的头节点是一个空的哨兵节点不保存实际线程。真正的第一个等待线程排在队列的第二个位置。每个节点的等待状态waitStatus记录了它是否需要被唤醒、是否已经取消等关键信息。Node节点的waitStatus有以下几个值记住它们对理解后续源码至关重要状态值含义触发时机0初始状态节点刚创建时-1 (SIGNAL)后继节点需要被唤醒当前节点释放锁时要唤醒下一个1 (CANCELLED)节点已取消线程放弃等待或发生中断-2 (CONDITION)节点在条件队列中使用Condition时-3 (PROPAGATE)共享模式下需要向后传播共享锁的释放场景接下来看入队的核心操作addWaiter。3.2 addWaiter入队源码拆解private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); // 快速尝试入队把新节点设置在尾节点之后 Node pred tail; if (pred ! null) { node.prev pred; // CAS把新节点设为新的尾节点 if (compareAndSetTail(pred, node)) { pred.next node; return node; } } // 快速失败后进入自旋入队 enq(node); return node; }代码很好理解先取当前的尾节点把新节点的prev指向它然后通过CAS把tail更新成新节点。如果成功就把老尾节点的next指向新节点入队完成。这个流程有一个很典型的“先改自己的prev再CAS更新尾指针最后修复老节点的next”三步走顺序顺序不能乱这是并发安全的关键。如果CAS失败说明有其他线程也在入队这时候进入enq方法自旋重试private Node enq(final Node node) { for (;;) { Node t tail; if (t null) { // 队列为空需要初始化 if (compareAndSetHead(new Node())) tail head; } else { // 队列不为空CAS入队 node.prev t; if (compareAndSetTail(t, node)) { t.next node; return t; } } } }这里的细节值得单独拿出来说。当队列还是空的时候第一步不是直接把新节点设为头节点而是先创建一个空的哨兵节点new Node()作为head。这个节点的thread字段是null纯粹占位用。为什么要这样设计因为AQS的解锁逻辑里唤醒后继节点时需要通过头节点去找。如果头节点就是一个真实线程释放锁之后它的thread就是null节点状态还要做一次清理和偏移非常麻烦。用一个固定的哨兵节点可以让所有等待线程节点的处理逻辑完全统一。这也解释了为什么你在debug时队列的head总是一个看起来“没意义的空节点”。3.3 acquireQueued排队之后还要循环抢一遍入队只是排上号了真正的等待发生在acquireQueued方法里。这段代码是整个AQS等待逻辑的心脏final boolean acquireQueued(final Node node, int arg) { boolean failed true; try { boolean interrupted false; for (;;) { // p是当前节点的前驱节点 final Node p node.predecessor(); // 如果前驱是head说明当前节点排到了第一位再尝试获取锁 if (p head tryAcquire(arg)) { setHead(node); // 获取成功把当前节点设为新头节点 p.next null; // 帮助GC failed false; return interrupted; } // 获取失败检查是否需要阻塞 if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }这段代码有几个关键点不是排到队首就能拿锁。只有当前节点的前驱节点是head它才有资格再尝试tryAcquire。即便你是队列第二个人前驱是哨兵头也只是获得抢锁的资格抢不抢得到还要看CAS结果。这是一个for循环也就是线程在park之前会一直自旋重试。这是AQS一个很有意思的优化刚入队的节点先不立即阻塞而是先自旋几次试试因为前驱节点可能很快就释放锁了。只有自旋也拿不到才真正进入阻塞状态。阻塞通过LockSupport.park实现。这个方法会让线程进入WAITING状态不消耗CPU等待被unpark唤醒。理解这个for循环你就理解了AQS“先自旋再阻塞”的混合等待策略。这种设计避免了线程频繁在用户态和内核态之间切换是性能优化的一个经典手法。4. 阻塞与唤醒从排队区到办事大厅的信号传递排队线程不能干站着消耗CPU得进入阻塞状态等前面的线程办完事来叫醒它。这个过程涉及shouldParkAfterFailedAcquire、parkAndCheckInterrupt和unparkSuccessor三个核心方法。4.1 shouldParkAfterFailedAcquire前驱节点的状态裁决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 { // 前驱状态是0或PROPAGATECAS设置为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }这个方法做的事情是为当前节点的阻塞做准备同时确保它的前驱节点处于“有能力唤醒后继”的状态。什么时候会返回true当前驱的waitStatus是SIGNAL也就是-1时。SIGNAL是一个承诺当前驱节点释放锁或被取消时必须唤醒它的后继节点。有了这个承诺当前节点就可以安心地park了。什么时候返回false有两种情况前驱状态是0或者PROPAGATE这时需要先CAS把状态改成SIGNAL告诉前驱“你待会要负责叫醒我”。改完之后这次循环还不能park需要再走一轮acquireQueued循环再尝试一次抢锁抢不到再park。前驱状态是CANCELLED大于0说明前驱已经放弃了需要继续往前遍历跳过所有已取消的节点直到找到一个活跃的节点然后重新建立链表关系。这里有个耐人寻味的设计为什么要先设置SIGNAL再park而不是直接park原因在于并发安全性。如果当前线程直接park而前驱恰好在park之前就释放了锁它发现后继的waitStatus不是SIGNAL就不会去唤醒当前线程可能永远睡死过去。只有先把SIGNAL状态设置好保证“前驱释放锁时必须唤醒我”然后才park才能避免这种竞态。这个顺序问题恰恰是很多手写锁代码容易踩坑的地方。4.2 parkAndCheckInterrupt与锁中断的真相park只是阻塞线程唤醒后还要检查线程是否被中断过。看这个方法的实现private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }LockSupport.park会让线程进入阻塞状态被唤醒时有两种可能被unpark主动唤醒或者被interrupt打断。无论是哪种情况park方法都会返回。返回之后调用Thread.interrupted()这个方法有两个作用一是返回线程的中断标记位二是把中断标记清除。为什么要清除因为AQS的acquireQueued在拿到锁之后如果发现发生过中断不会立即抛异常而是会把interrupted标志保存在局部变量里等到整个acquire流程结束再调用selfInterrupt()把中断状态重新设置回去。这意味着一个容易被误解的点lock()方法不响应中断。你中断一个正在等待锁的线程它不会抛出InterruptedException也不会放弃等待只是会在成功拿到锁之后重新设置中断标志。真正能响应中断的是lockInterruptibly()方法它会在park返回后检测中断并且直接抛出InterruptedException。4.3 释放锁时如何唤醒后继线程释放锁的入口是releasepublic final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); return true; } return false; }记住了一个关键判断条件h ! null h.waitStatus ! 0。如果头节点状态是0说明没有后继需要唤醒或者后继还没来得及设置SIGNAL状态。这里头节点通常是哨兵节点它的waitStatus代表的是队列中第一个真实等待节点的状态协议。看unparkSuccessor的实现private void unparkSuccessor(Node node) { int ws node.waitStatus; if (ws 0) compareAndSetWaitStatus(node, ws, 0); Node s node.next; // 如果后继节点为空或已取消从tail向前找 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); }这个方法有两个值得注意的细节为什么要从tail往前遍历找后继因为在并发入队过程中节点的next指针可能还没来得及设置前一步我只是通过CAS把新节点设置为tail但老tail的next是之后才补上的。所以从头往后找可能找不到真实节点而从tail往前找prev指针是入队时最先设置的一定可靠。唤醒的标准是waitStatus 0也就是说只有CANCELLED节点会被跳过其他状态的节点都可以唤醒。这段逻辑用生活场景比喻办事大厅里出来一个人保安先看队首的人有没有放弃排队CANCELLED如果放弃了就跳过找到下一个还在等的人直接喊他的名字进来。5. 公平锁与非公平锁排队规则决定了效率与公平ReentrantLock有两个实现类NonfairSync和FairSync名字已经说明了区别。前面讲的都是非公平锁现在专门看看公平锁的实现差异。5.1 fairSync的tryAcquire先看队列再决定是否抢公平锁的lock实现和非公平锁几乎一样都先走acquire(1)差别在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()); }这个方法判断队列中是否存在比自己更早到达的等待线程。如果队列为空或者队列中第一个等待节点就是当前线程自己返回false可以抢锁。否则返回true说明有人比你来得早你必须去排队。这就保证了FIFO的公平性先来后到谁先排队谁先拿到锁。5.2 公平锁的性能陷阱为什么默认是非公平既然公平锁更“公平”为什么ReentrantLock默认不使用公平锁核心原因是上下文切换开销。非公平锁在锁被释放的瞬间允许新来的线程直接CAS抢锁省去了入队、阻塞、唤醒再竞争这一整套流程。在锁竞争并不激烈的情况下非公平锁的性能可以比公平锁高出几个数量级。但非公平锁也有代价如果锁竞争非常激烈可能出现“插队线程永远成功排队线程饿死”的情况。虽然概率不高因为插队线程CAS失败后也会去排队但长尾延迟问题是真实存在的。从实践角度我给一个经过多次压测验证的结论临界区执行时间短、线程数多优先用非公平锁吞吐量高。要求等待时间相对均匀、不允许饥饿用公平锁。混合场景拿不准先默认非公平锁用JFR或Arthas观察线程等待时间分布再决定是否切换公平模式。5.3 一个容易被忽视的细节公平锁的tryLock注意一个设计上的“不一致”even公平锁的tryLock()方法调用的仍然是非公平获取逻辑。看一下FairSync的源码protected final boolean tryAcquire(int acquires) { // 公平逻辑上面已经展示 } // 但lock()的tryLock方法是用这个 final boolean tryLock() { return nonfairTryAcquire(1); }这是从父类Sync继承的方法里面直接调用了nonfairTryAcquire。也就是说即使你声明的是公平锁调用tryLock()时依然可以插队。官方对这个行为没有特别明确的解释但从实践角度看tryLock本身是“尝试一下不行就放弃”的语义如果强制要求排队短超时的尝试几乎总是失败使用价值会大打折扣。了解这个差异在写依赖公平性的业务逻辑时要特别注意真的需要严格FIFO就老老实实用lock()别用tryLock()来探测。6. 实操用调试器“看到”AQS队列的真实状态学了这么多源码不动手验证等于白学。接下来带你把代码跑起来用断点看到真实的队列长什么样。6.1 搭一个最小复现环境准备一个简单的模拟场景三个线程同时竞争一把锁让第一个线程持锁时间足够长保证后两个线程一定进入排队状态。import java.util.concurrent.locks.ReentrantLock; public class AqsDebug { static final ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws Exception { Thread t1 new Thread(() - { lock.lock(); try { System.out.println(Thread.currentThread().getName() 抢到锁); Thread.sleep(10000); // 持锁10秒 } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }, 线程1); t1.start(); Thread.sleep(100); // 保证线程1先拿到锁 Thread t2 new Thread(() - { lock.lock(); try { System.out.println(Thread.currentThread().getName() 抢到锁); } finally { lock.unlock(); } }, 线程2); Thread t3 new Thread(() - { lock.lock(); try { System.out.println(Thread.currentThread().getName() 抢到锁); } finally { lock.unlock(); } }, 线程3); t2.start(); t3.start(); while (true) { Thread.sleep(1000); } } }6.2 用断点观察队列快照在acquireQueued的for循环内部打一个断点条件设为node的前驱等于head也就是线程即将获得锁的位置。当程序运行到断点处在IDE的Debug窗口里打开Variables面板找到AQS对象的head和tail字段展开。你会看到这样的结构信息head引用一个Node对象它的thread字段为null这是哨兵节点。head.next指向另一个Nodethread是线程2。如果线程3也还没被唤醒tail会指向thread为线程3的Node。每个Node的waitStatus可能是0、-1SIGNAL或1CANCELLED。这个可视化过程比看一百遍源码都直观。当你亲眼看到head哨兵、尾节点、SIGNAL状态是怎么串联起来的AQS就不再是一堆抽象概念了。6.3 用jstack看线程状态的印证光看内存结构还不够还需要看线程的运行时状态。调试时打开终端执行jstack命令jstack -l pid找到线程2和线程3的栈顶你会看到类似这样的输出线程2 #11 prio5 os_prio31 cpu101.34ms elapsed137.71s tid0x00007fa43e002800 nid0x6703 waiting on condition [0x000000030adce000] java.lang.Thread.State: WAITING (parking) at jdk.internal.misc.Unsafe.park(java.base17.0.11/Native Method) at java.util.concurrent.locks.LockSupport.park(java.base17.0.11) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(...)关键是WAITING (parking)这个线程状态说明线程已经成功park正在等待LockSupport.unpark来唤醒。如果你观察的是公平锁场景你还能通过线程在WAITING状态等待的时长直观感受到公平与非公平的等待差异。6.4 一个小实验验证非公平锁的插队行为在很多次的并发实验中我都观察到非公平锁的一个“戏剧性”现象高频率地获取和释放锁队列里明明有等待线程但新来的线程反复CAS成功导致等待线程长时间抢不到锁。你可以动手验证用非公平锁启动2个常驻等待线程再开一个循环用新线程高频抢锁。观察等待线程的耗时经常出现很大的波动。这个实验用JMH基准测试搭建最简单但我更推荐直接用脚本在主线程循环lock/unlock几千次然后打印各个线程抢到锁的先后顺序。实测下来你会发现插队现象远超你想象。7. 常见问题与排查技巧实录最后把我在实际使用ReentrantLock和阅读源码过程中遇到的高频问题整理成表格这些坑基本都是书上看不到的经验。7.1 高频问题速查表问题原因分析解决方案lock之后忘记unlock逻辑分支提前return或异常抛出让finally没执行lock之后立刻写try/finally不要在中间插入任何可能return的代码同一个锁多次lock但只unlock一次state计数没有归零锁永远不会释放严格配对lock/unlock或者用lock代替可重入次数太多导致state溢出重入次数超过int最大值nextc溢出为负数这是极端情况但源码里确实有保护throw new ErrorlockInterruptibly被中断后锁资源没释放中断抛异常后跳过finally块始终在try-finally中释放锁异常路径也要覆盖公平锁下使用tryLock发现还是插队tryLock内部走的是非公平获取逻辑需要严格公平时改用lock()或自己封装带公平调度的新方法等待锁的线程永远无法唤醒可能前驱节点被取消后后继没有正确接管升级到较新JDK版本旧版本有过极端场景下的bug修复debug时看到节点waitStatus长期为1线程超时或被中断取消了等待检查业务代码是否有主动取消锁等待的逻辑7.2 排查锁竞争的三个实用工具遇到锁竞争问题我通常按这个顺序排查第一用jstack检查线程状态。如果大量线程处于WAITING (parking)状态说明它们在锁队列里等待。结合栈顶找到具体是哪把锁。第二用Arthas的thread命令可以显示线程的CPU占用和阻塞时间还能直接看到某些线程在等待什么锁。命令就是thread -b看阻塞情况thread --state WAITING看所有等待线程。第三用JFR录制锁竞争事件。JFR的Lock Instant事件能记录每次锁竞争的发生时间、等待线程、持有线程和等待时长。这是定位锁竞争和瓶颈的最精确手段。7.3 关于AQS源码学习的一点个人经验源码阅读最忌讳从头到尾逐行死磕。我的经验是先看核心方法的调用链再顺着一个典型时序去理解状态流转最后再回到细节。比如你只需要理清一条线——lock()、acquire()、tryAcquire()、addWaiter()、acquireQueued()、parkAndCheckInterrupt()、unlock()、release()、tryRelease()、unparkSuccessor()——这一条主链路走通AQS的骨架就已经在你脑子里了。剩下的Condition、共享模式、超时获取都是在这个骨架上做扩展理解起来会快得多。另外我建议抽出时间自己动手实现一个简化版AQS只支持最基本的互斥锁功能不要求性能能跑通多线程竞争就行。我当初花了大概一个下午写出一个约150行的迷你版代码量不多但写完后再回看JUC源码里的实现那些之前觉得绕的设计突然就顺了。很多代码层面的精巧设计只有当你自己踩过坑才会真正理解它的用意。最后再分享一个我自己的习惯凡是使用ReentrantLock的项目我都会在提交前跑一轮简单的JMH压测单独对比一下公平模式和非公平模式下的吞吐量和等待时间分布。这个习惯帮我发现过好几次因为误用公平锁导致的长尾延迟问题。记住并发编程里任何“看起来更安全”的设计都可能隐藏着巨大的性能代价。最好的做法永远是先认清业务需求再选择对应的锁策略最后用数据验证你的选择。
延伸阅读

更多相关文章

2026/10/16 8:12:03

PDF/Word一键转可编辑PPT:三层解析与AI语义压缩实践

很多做汇报材料的朋友都体会过这种痛苦:拿到一份行业白皮书或者内部制度文件,PDF有七十多页,每一段都写得密不透风。你需要从中划出五六个重点,换成口语化的表述,再用三四十页 PPT 把意思讲清楚。手动做这件事&#xf…

2026/10/16 8:12:03

Python网络设备自动巡检脚本:多厂商支持、异常告警与PDF报告

简介:本资源是一套基于Python实现网络设备自动化巡检的轻量级脚本工具包,面向IT运维工程师、网络管理员及Python初学者,解决大型网络环境中人工巡检效率低、易出错的核心痛点。资源共7个文件,含5个INI配置文件(分别定义…

2026/10/16 8:12:03

MQTT与InfluxDB组合实战:物联网数据采集存储方案

前段时间做了个物联网数据采集的小项目,设备端每隔几秒上报一次温湿度、电量和位置信息,一天下来几百万条数据。一开始团队想用 MySQL 存,结果图表查询慢到让人怀疑人生。这种情况在物联网里太常见了,高频率、按时间流动的传感器数…

2026/10/16 11:35:14

Java Lock 与 synchronized 选型指南:ReentrantLock 与 Condition 实战

1. 从 synchronized 到 Lock:为什么需要另一把锁刚接触 JUC 并发包那会儿,我脑子里一直有个疑问:既然synchronized已经能解决线程安全问题,为什么还要搞出一个Lock接口?后来在一个订单扣减库存的模拟场景里踩了坑&…

2026/10/16 11:35:14

操作系统文件管理实验:从零实现文件系统模拟器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 11:35:14

Claude Code自动提交代码到GitHub:配置、实操与踩坑记录

最近折腾了一套“让 Claude Code 自动向 GitHub 提交代码”的流程,从最初的半信半疑,到后来真让它独立完成了一次完整的代码生成、测试、提交、推送,中间踩了不少坑,也摸清了一些门道。这篇文章就把整个思路、配置过程、实操记录和…

2026/10/16 11:35:14

C#工控仪表控件库:WinForms实时数据绑定与DPI自适应渲染

简介:本资源是一套面向工控、组态软件开发及.NET桌面应用初学者的C#仪表控件实战示例,聚焦圆表、直表(测量标尺)、旋钮、开关与指示灯等高复用性UI组件的集成与定制。资源完整包含可直接运行的演示程序(CShapeExample.…

2026/10/16 11:30:12

HCIA智能计算101-164题号考点拆解:从硬件到场景的备考逻辑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑