ReentrantLock与AQS源码解析:从抢座位到队列机制 抢座位的场景我估计大家都经历过上课铃响前教室前排的好位置就那么几个来得早的人先坐下不来的人位置空着一旦有人离开座位旁边等的人立刻补上去。Java里的ReentrantLock干的事跟这个场景几乎一模一样——大家争着抢一个共享资源的使用权抢不到的人就只能排队等等前面的人用完再按顺序进去。但神奇的是这套抢座位逻辑并不是ReentrantLock自己实现的而是由一个叫AQSAbstractQueuedSynchronizer的排队框架在背后撑着。我写过挺多并发代码也被ReentrantLock的源码折磨过很多次。说实话刚开始看acquire、acquireQueued、shouldParkAfterFailedAcquire这串方法名时人是很懵的方法调用一层套一层感觉像进了迷宫。但后来我自己把整个过程拆成抢座位→排队→被叫号→进去办事→出来叫下一位这五个动作后突然就通了。这篇博文就按这个思路来从源码级别把ReentrantLock和它依赖的AQS队列机制一层层剥开适合正在学Java并发、被ReentrantLock源码卡住的同学也适合想彻底搞懂AQS面试原理的从业者。我会直接贴JDK 8版本的源码然后加白话注释最后统一梳理各种常见问题。读完你会发现AQS就是一个结构很整齐的教室排队规则没有想象中那么神秘。1. 先说结论ReentrantLock到底锁住了什么1.1 从抢座位理解锁的三要素平时写并发代码我们最常用的两个锁是synchronized和ReentrantLock。synchronized是JVM层面的关键字ReentrantLock是JDK提供的类底层核心是AQS。但不管是哪个锁抽象来看都逃不过三样东西锁状态当前这个资源是否被别人占用了占用了几次。对应到代码里就是AQS里的state字段。等待队列没抢到锁的线程排成一队按顺序等着。对应到代码里就是AQS内部的CLH双向队列。阻塞与唤醒排队的人不能一直在那干转CPU得让出执行权睡一会儿等前面的线程释放锁后再叫醒自己。这一步用LockSupport.park/unpark实现。举个例子假设系统里同时有10个线程要执行写一条订单记录这个操作但数据库层面的唯一性约束要求同一时间只能有一个线程写。第一个线程进来看到state是0CAS修改为1成功直接进临界区干活后面9个线程进来CAS都失败了它们就各自生成一个Node节点挂到队列尾巴上然后调用LockSupport.park让自己阻塞。第一个线程干完活把state改回0再找到队列头部后面的第一个有效节点调用unpark唤醒它。这整个过程就是ReentrantLock加锁和释放锁的完整生命周期。你说的抢座位本质上是线程之间的竞争而队列是竞争失败后的秩序保障。1.2 ReentrantLock和synchronized怎么选网上有很多对比我这里只摆几个我实际编码中最关注的差异点用表来看比较直观对比维度synchronizedReentrantLock加锁释放方式自动异常自动释放手动必须在finally里unlock是否可重入是是能否响应中断不能线程中断只会标记lockInterruptibly可以响应中断是否支持超时不支持tryLock(timeout)支持是否区分公平性非公平支持公平/非公平两种模式条件队列数量单一等待集一个锁可以创建多个Condition底层实现偏向锁、轻量级锁、重量级锁监视器AQSCASLockSupport实际项目里如果只是简单的并发控制我基本优先用synchronized它不用手动释放出异常也不会死锁。但一旦需要锁等待超时、可中断获取锁、或者多个等待队列比如生产者消费者里区分没货和没空位这样的能力synchronized就很吃力这时候ReentrantLock就是更好的选择。举个例子我做过一个库存扣减的接口要求抢购时等待锁不能超过100毫秒超过就直接返回失败用synchronized根本做不了这种超时控制只能上ReentrantLock加tryLock(100L, TimeUnit.MILLISECONDS)。这就是选型的关键理由。2. AQS的骨架一条双向队列和一个状态变量2.1 state、head、tail和Node节点先看AQS这个类里最核心的字段JDK 8的源码是这样的public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer { // 锁状态0表示没锁大于0表示已被占用可重入时state会累加 private volatile int state; // 队列头节点代表当前持有锁的线程其实是“已经获得锁的虚拟节点” private transient volatile Node head; // 队列尾节点每次新线程排队就挂在tail后面 private transient volatile Node tail; }还有一个容易被忽略但很重要的字段在父类AbstractOwnableSynchronizer里public abstract class AbstractOwnableSynchronizer { // 当前持有锁的线程 private transient Thread exclusiveOwnerThread; }然后把目光看向内部的Node类。AQS的等待队列就是由这些Node串成的一条双向链表每个Node代表一个等待中的线程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; // 条件队列或共享标记时使用 }如果你把这些字段套回教室排队场景里理解成本会低很多head队列最前面的虚拟节点可以理解成正在办事的窗口它自己并不代表某个真实等待的线程。tail队列最后面的节点新来的线程通过CAS插到它的后面。next和prev排队的人一前一后互相牵着方便从前往后和从后往前双向查找。waitStatus最重要的就是SIGNAL和CANCELLED。SIGNAL表示我这个位置的人在离开之前有义务叫醒后面的人CANCELLED表示这个人等不下去了已经退出排队。2.2 为什么AQS要选双向链表而不是数组不少第一次看源码的人会问排队而已用数组不行吗其实这里选双向链表是经过考量的三个原因很关键第一并发插入的复杂度低。数组的插入要么扩容搬移要么维护一个写指针做并发协调还要考虑元素移动成本高。而双向链表只需要CAS把新节点设置为尾节点O(1)就能完成并发入队。第二取消排队时需要快速操作前驱和后继。比如某个线程等了很久不想等了要把自己从队伍里摘掉。这需要同时改前驱节点的next和后继节点的prev只有双向链表能在O(1)时间里找到前后节点并完成断链。第三唤醒时需要精准定位线程。释放锁时我们要找到队首后面第一个能唤醒的线程直接在节点上拿到thread字段然后LockSupport.unpark精准唤醒。数组还要额外存一份线程引用本质上差不太多但链表表达得更自然。既然底层是一套这么清晰的排队机制那加锁和释放锁的源码流程就值得好好啃一遍。下一节我们跟着源码走一遍看看线程到底是怎样一步步完成抢座位的。3. 加锁源码走读从acquire开始3.1 非公平锁抢座位的瞬间compareAndSetStateReentrantLock默认构造的是非公平锁我们看它的加锁入口// ReentrantLock.java public void lock() { sync.lock(); }这里的sync是ReentrantLock内部的一个Sync实例分为NonfairSync和FairSync。NonfairSync的实现是这样的static final class NonfairSync extends Sync { private static final long serialVersionUID 7316153563782803691L; final void lock() { // 一上来就CAS抢一下完全不管队列里有没有人排队 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } }注意这里只有两行逻辑却藏着非公平锁的核心设计compareAndSetState(0, 1)用CAS把state从0改为1成功就说明锁到手了。这个操作是原子的多线程同时进来也只有一个能成功。成功后还要调用setExclusiveOwnerThread记录当前线程这是为了后面支持可重入。如果CAS失败就走acquire(1)。上来就抢这个动作就是非公平锁和公平锁最大区别的根源。哪怕队列里已经排了十个人新来的线程也先CAS试一次抢到了就插队。这看起来不太公平但实际换来的是高吞吐——因为很多场景下锁的持有时间极短刚释放锁的线程大概率还正在运行中让它立刻重新拿到锁省掉一次线程挂起和唤醒的开销。3.2 抢不到怎么办acquire的完整路径CAS失败后走进AQS.acquirepublic final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }这四行代码被誉为AQS最经典的缩影。拆开看tryAcquire(arg)再给一次机会尝试获取锁。这一次可不是简单CAS了里边还包含重入判断和非公平逻辑。如果成功直接返回。addWaiter(Node.EXCLUSIVE)如果还失败就把当前线程封装成独占模式的节点挂到队列尾部。acquireQueued(node, arg)进入排队等待状态在循环里反复尝试获取锁同时处理中断和阻塞。selfInterrupt()如果在等待过程中线程被中断过这里会把中断标志补上。来看NonfairSync.tryAcquire它实际上调用的是Sync.nonfairTryAcquireprotected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { if (compareAndSetState(0, acquires)) { // 队列中没人持有就再CAS一次 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。比如连续两次lock()嵌套state从0变1再从1变2释放的时候也要对应递减两次。重入机制的本质就是给同一个线程多发了几个入场券每次lock都计入state每次unlock都减掉最后减到0才真正释放。3.3 公平锁的区别在于看队首公平锁对应的入口是这样的static final class FairSync extends Sync { final void lock() { acquire(1); // 公平锁不先CAS抢直接走完整流程 } 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; } }最大的差异就是tryAcquire里多了一个hasQueuedPredecessors()判断public final boolean hasQueuedPredecessors() { Node t tail; Node h head; Node s; // 关键判断队列里是否有人在排队而且排在最前面的不是当前线程 return h ! t ((s h.next) null || s.thread ! Thread.currentThread()); }这个判断翻译过来就是如果没有人在排队或者排在队首的就是我那我才有资格去抢锁。否则就算锁此刻是空闲的我也必须老老实实排到队尾不能插队。hasQueuedPredecessors里有个细节(s h.next) null这个条件是为了处理一种并发边界——当head和tail都初始化完毕但恰好有一个线程还没把新节点接入h.next时。这个条件是用于防止信号漏掉的不能去掉。很多人在面试时会被这道源码题问倒记住这段逻辑背后是在处理入队但尚未完成链接的瞬间态即可。4. 排队后的自我修养acquireQueued为什么这么难懂4.1 自旋、park与interrupt当线程通过addWaiter进入队列之后真正复杂的逻辑都集中在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)) { // 前驱节点是head说明我已经排到队首了再试一次拿锁 setHead(node); p.next null; // help GC failed false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) parkAndCheckInterrupt()) interrupted true; } } finally { if (failed) cancelAcquire(node); } }这个for(;;)定义了一个无限循环注意它跟传统的自旋锁是有本质区别的。传统自旋锁是空转CPU等锁而这里的循环大多数时间都在park睡眠醒来后重新检查条件所以它更像一个阻塞-唤醒-再判断的回合制循环。循环里做的事情可以概括为四步拿到前驱节点p。如果p head说明当前线程是队列里排在最前面的真实等待者这时再调用tryAcquire抢一次。抢成功就把自己设为head同时清掉thread引用因为head节点语义上不代表某个线程而代表锁已占用。如果p ! head或者抢锁失败进入shouldParkAfterFailedAcquire判断是否应该阻塞。如果应该阻塞就调用parkAndCheckInterrupt真正挂起线程。setHead这个方法值得看一眼private void setHead(Node node) { head node; node.thread null; node.prev null; }它把当前节点升级为新head但这时的节点其实不再是排队的人而是正在用锁的人。它的thread被清空prev也被设为null标志着它脱离了队列的排队部分。4.2 shouldParkAfterFailedAcquire如何确定该不该睡shouldParkAfterFailedAcquire是前面加锁流程里最容易让人忽略但极其重要的方法private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws pred.waitStatus; if (ws Node.SIGNAL) return true; // 前驱承诺会唤醒我可以放心睡 if (ws 0) { // ws 0 表示CANCELLED跳过所有取消的节点 do { node.prev pred pred.prev; } while (pred.waitStatus 0); pred.next node; } else { // 把前驱的waitStatus CAS成SIGNAL表示“前驱你走的时候记得叫我” compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }这个方法解决的核心问题是睡之前要确保有人会在合适的时候把我叫醒。这里的有人指的就是自己的前驱节点。当前驱的waitStatus变成了SIGNAL相当于它在说我要么会拿到锁要么会在让步时唤醒你。只有确认这一点后当前线程才能安心调用park。如果前驱节点已经被取消waitStatus 0那就顺着prev指针往前跳跳过所有取消节点重新建立连接。这是队列中清理脏数据的方式。接着看parkAndCheckInterruptprivate final boolean parkAndCheckInterrupt() { LockSupport.park(this); // 线程阻塞可在unpark或者interrupt时返回 return Thread.interrupted(); // 返回是否被打断并清除中断标志 }LockSupport.park和synchronized的wait不一样它不需要持有锁就能阻塞线程也不会抛出InterruptedException。线程挂起后有两种方式可以被唤醒别的线程调用unpark或者当前线程被interrupt。4.3 中断处理两种模式对比lock()和lockInterruptibly()对中断的处理是很多开发者踩坑的地方。看源码位置就知道差异lock()走的是acquire(1)在acquireQueued循环里parkAndCheckInterrupt检测到中断后仅仅是interrupted true记录一下然后继续循环抢锁。等真正拿到锁之后acquireQueued返回trueacquire里的selfInterrupt()再把中断标志设置回去。这叫响应中断记录但不立即退出。lockInterruptibly()走的是acquireInterruptibly(1)在检测到中断时会直接抛出InterruptedException然后线程退出等待状态。这叫可中断获取。两种模式各有用途。lock()适合不管怎样我都要拿到锁中断等我拿到锁后再处理的场景lockInterruptibly()适合用户取消任务时我应该立刻退出等待的场景。如果你在待办任务列表里想取消一个正在等锁的任务用lock()就会发现线程根本不会停必须等锁真正到手才响应取消信号。到这一步加锁路径基本走通了。接下来看释放锁的路径其实比加锁路径简单不少。5. 释放锁源码走读从state减一到唤醒队首5.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; }再看Sync.tryRelease的实现protected final boolean tryRelease(int releases) { int c getState() - releases; // state减1 if (Thread.currentThread() ! getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 只有持锁线程能释放 boolean free false; if (c 0) { free true; // 减到0锁真正释放 setExclusiveOwnerThread(null); } setState(c); return free; }这里的逻辑非常直白每次unlock()调用就会把state减1。如果state还没到0说明只是释放了一层重入锁仍然被当前线程持有不需要唤醒任何人。如果减到了0说明全部释放完清空exclusiveOwnerThread返回true接下来才进入唤醒流程。这里有个很重要的隐蔽点非持锁线程调用unlock()会抛IllegalMonitorStateException。这跟synchronized遇到非持锁线程执行到同步块外的IllegalMonitorStateException是一种安全保护防止一个线程误释放了别的线程的锁。我见过有人把lock()和unlock()放在不同方法里中间又穿插了别的线程操作结果就踩了这个异常。5.2 unparkSuccessor从tail往前找的原因锁释放后该叫醒谁答案不是直接叫醒head.next而是用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) { // 如果后继为null或者已经被取消 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); // 唤醒找到的线程 }很多第一次读源码的人会疑惑为什么next不为空且有线程还要从tail往前遍历这是因为在并发入队时addWaiter通过CAS把节点设置成tail是第一步而建立prev和next的完整双向链接是第二步。当一个节点刚通过CAS成为tail、但还没有设置前驱和后继字段时从head往后看可能暂时断链。从tail往前遍历则永远不会断因为prev的设置在节点入队时是先从尾部方向确保的。这个细节其实体现了并发编程里一个通用原则在无锁数据结构中从后往前遍历往往是更安全的方向。我在排查一些并发队列问题时也遇到过类似情况从错误方向遍历导致找不到节点理解了AQS这个设计之后再看其他无锁队列就有种触类旁通的感觉。6. 从抢座位到完整用法tryLock、超时锁与条件队列6.1 tryLock()和tryLock(timeout)如何优雅地打卡lock()的特点是不撞南墙不回头拿不到锁就死等。但实际业务里很多时候我们想要的是抢不到就算了干点别的。这时就要用tryLock了。tryLock()走的是Sync.nonfairTryAcquire(1)只尝试一次拿到锁返回true拿不到立刻返回false完全不排队不阻塞。Lock lock new ReentrantLock(); if (lock.tryLock()) { try { // 拿到锁做业务 } finally { lock.unlock(); } } else { // 没拿到锁做其他处理 }而tryLock(timeout, unit)则支持等一会儿try { if (lock.tryLock(200, TimeUnit.MILLISECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } } else { throw new RuntimeException(获取锁超时请稍后重试); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(线程被中断放弃获取锁); }这个方法在分布式服务里用来做本机互斥限流非常合适。比如定时任务集群部署时同一台机器上多个线程都可能触发同一个任务用tryLock配合超时能避免任务重复执行同时也不会因为锁等待拖死线程。有个经验说下tryLock(timeout)内部会先走一次非公平的tryAcquire也就是说即使你用的是公平锁tryLock(timeout)也会在超时等待过程中先去抢一把这其实是JDK实现的一个小不公平点。如果业务要求绝对公平就要注意这个行为。6.2 lockInterruptibly和Condition给等待队列插上翅膀ReentrantLock比synchronized强大得多的一个地方就是支持多个Condition。所谓Condition可以理解成锁内部的独立等待队列。synchronized只有一个等待集而ReentrantLock可以new出多个Lock lock new ReentrantLock(); Condition notEmpty lock.newCondition(); // 非空条件 Condition notFull lock.newCondition(); // 非满条件经典的生产者消费者就能这样设计class GoodsQueue { private final Lock lock new ReentrantLock(); private final Condition notEmpty lock.newCondition(); private final Condition notFull lock.newCondition(); private String[] items new String[10]; private int count; public void put(String item) throws InterruptedException { lock.lockInterruptibly(); try { while (count items.length) { notFull.await(); // 队列满了等“非满”信号 } items[count] item; notEmpty.signal(); // 已经放入商品唤醒“非空”等待者 } finally { lock.unlock(); } } public String take() throws InterruptedException { lock.lockInterruptibly(); try { while (count 0) { notEmpty.await(); // 空了等“非空”信号 } String item items[--count]; notFull.signal(); // 已经取出商品唤醒“非满”等待者 return item; } finally { lock.unlock(); } } }这里有几个要点必须记住await操作会释放锁让其他线程能进入临界区。这是它和sleep最大的区别。await被唤醒后回到while条件重新检查不能只用if判断。因为存在虚假唤醒的可能必须循环判断条件。signal只是把等待者从条件队列移到同步队列并不是立刻唤醒执行真正让线程跑起来还得等它拿到锁。lockInterruptibly配合await可以让生产者消费者在外部中断时及时退出。6.3 Condition的await和signal内部到底做了什么ConditionObject是AQS的内部类它的await实现大概分这几步当前线程封装为一个Node节点waitStatus CONDITION挂到条件队列末尾。释放当前线程持有的所有锁实际上是fullyRelease把state清0并唤醒下一个同步队列节点。进入循环检查当前节点是否已经转移到同步队列如果没有就park自己。被signal后节点从条件队列移到同步队列然后走acquireQueued相同的路径参与锁竞争。而signal的核心是transferForSignal它把条件队列中的第一个节点移除waitStatus从CONDITION改为0然后通过enq把它重新挂到同步队列尾。之后这个节点就回到了普通锁竞争的逻辑里。从源码我们能看出来Condition本质上就是AQS内部维护的另一个等待队列跟同步队列一样Node复用了同一套结构。理解了这一点再看signal和await就不会觉得它们是什么黑魔法了。7. 高频问题与排查技巧实录7.1 问题速查表平时开发和面试里围绕ReentrantLock和AQS的高频问题我整理成了一张表基本覆盖了大多数场景现象/问题可能原因排查思路与解决办法程序卡住不动但不崩某个线程持锁后一直不释放先jstack导线程栈找WAITING状态的线程定位是否在park或lock处抛出IllegalMonitorStateExceptionunlock()的线程不是持锁线程检查lock和unlock是否配对是否在finally里释放锁竞争激烈时吞吐降低非公平锁虽然能插队但仍可能有很多线程被挂起唤醒考虑减小锁粒度、用LongAdder替代AtomicLong、或改用无锁结构同一线程重入次数过多递归加锁没有对应释放用Thread.holdsLock()辅助排查或代码审查时关注递归方法内的锁await()之后一直醒不来没有对应的signal/signalAll或信号在条件满足前就丢了检查signal是否在finally之外的逻辑分支遗漏await要用while循环包裹高并发下排队线程饿死使用非公平锁新线程反复插队改为new ReentrantLock(true)但要注意吞吐下降的副作用线程中断后仍在等锁调用的是lock()而不是lockInterruptibly()如果需要中断及时退出使用lockInterruptibly或tryLock(timeout)出现Error: Maximum lock count exceededstate累加溢出递归重入太多了需检查锁的获取次数是否失控一般不会到这步7.2 几个源码细节的加分点很多人在看完AQS源码后还是容易忽略一些边角细节。我这里补充几个我在代码评审和面试中经常谈到的点第一state是int类型存在溢出风险。nextc c acquires这里如果溢出会变成负数JDK源码里直接抛Error而不是Exception。虽然正常业务不会把state累加到20亿次但实现自定义同步器时这点仍然值得注意。第二AQS并不等于ReentrantLock。AQS是一个同步器框架ReentrantLock只是其中一种用法。Semaphore、CountDownLatch、ReentrantReadWriteLock这些类内部都借助了AQS的state和队列机制只是修改了tryAcquire/tryRelease等模板方法的语义。比如Semaphore拿的是计数CountDownLatch等的是计数器归零。理解了AQS这套模板方法模式再看其他并发工具源码会轻松很多。第三LockSupport.park是可以被interrupt唤醒的。这意味着acquireQueued循环中即使线程被打断也有可能提前从park中返回然后继续抢锁。这也是为什么parkAndCheckInterrupt需要用Thread.interrupted()把中断标志清掉否则下次park会立即返回线程就无法真正睡眠了。第四volatile只在读状态时保证可见性状态修改必须靠CAS。这是很多初学者容易搞混的state被volatile修饰但volatile只保证读了不会脏要保证并发修改安全还是得靠compareAndSetState。写state的时候不走CAS而直接setState是在已经持有锁的前提下进行的此时没有并发竞争所以才可以直接写。这个锁内写、锁外读的配合实际正是很多并发数据结构的常规操作。7.3 一些实际开发中的建议源码看完了最后还是说说实践建议这些是我在项目里踩过坑之后总结出来的。首要的一条unlock()永远放在finally里。不管业务代码是正常return还是抛异常锁最终都要释放否则下一次线程就会一直停在队列里。哪怕只是漏了一个分支死锁都在所难免。第二条锁粒度尽量小。不要在持锁期间做耗时操作比如远程调用、数据库批量查询、大文件IO。锁本身不慢慢的是把太多事塞到锁里。抢锁时间建议控制在毫秒级更长时间的操作要么拆分要么用无锁设计。第三条能用tryLock就用tryLock。尤其是做资源竞争控制的场景等待锁的时间是造成线程堆积的常见原因。给tryLock设置一个合理超时配合快速失败返回系统稳定性会好很多。第四条Condition和while循环搭配使用永不使用if。JDK文档里明确提示await存在虚假唤醒虽然实际发生概率很低但while才是安全写法。这是规范不是理论。第五条调试AQS问题优先看线程栈。如果一个线程死在lock()里jstack会显示它在park状态如果死在await()里会显示在ConditionObject.await。栈顶基本就能定位到卡点。再配合打印持锁线程基本就能在几分钟内锁定问题。我个人感受最深的一点看AQS源码最好跟着业务场景走不要对着定义死磕。把ReentrantLock当成一个具体业务系统把AQS当成支撑它的排队基础设施理解起来就顺多了。实际开发中我也越来越倾向于把这类并发机制抽象成场景来记忆而不是纠结每一行代码本身。最后再分享一个小技巧如果你在代码评审时看到有人用ReentrantLock却没用lockInterruptibly或tryLock不妨多问一句这个锁等待能接受多久往往这一问就能问出很多隐藏的设计问题。