
之前在团队内部做过一次JUC的分享正好讲到ReentrantLock整理完资料后发现很多同学对它的理解停留在“和synchronized差不多”这个层面但真要问到底层的AQS队列、公平非公平的实现差异、Condition的等待通知机制基本都答不上来。这也能理解平时业务代码里一个synchronized能解决90%的互斥需求ReentrantLock作为进阶的并发组件如果面试不深挖确实容易被忽略。不过既然跳到“解析与面试题”这个主题我就不打算只讲怎么用了。ReentrantLock可以说是理解JUC整个并发体系的敲门砖AQS同步队列、CLH锁思想、LockSupport.park/unpark、CAS状态流转这些核心概念全都能在它的源码里串起来。本篇是第一篇我是按这个思路组织的先看ReentrantLock的整体设计它和synchronized的关键差异到底在哪再拆源码从lock()、unlock()到公平锁和非公平锁的实现对比最后是高频面试题的拆解每个问题尽量给到源码层面的依据而不只是背结论注意本文源码分析基于JDK 8/11的ReentrantLock实现不同版本细节略有差异但核心思想一致。建议读者在IDE里打开java.util.concurrent.locks.ReentrantLock源码对照阅读效果比只看文章好很多。1. ReentrantLock的整体设计思路1.1 它解决的是什么问题ReentrantLock本质上是一个“可重入的互斥锁”。什么叫可重入就是同一个线程可以多次获取同一把锁而不会被自己阻塞。举个例子public class Demo { private final ReentrantLock lock new ReentrantLock(); public void outer() { lock.lock(); try { inner(); } finally { lock.unlock(); } } public void inner() { lock.lock(); try { // do something } finally { lock.unlock(); } } }假设某个线程调用了outer()它在已经持有锁的情况下又进入inner()再次加锁。如果锁不具备可重入性这里就会直接死锁。而ReentrantLock通过“持有线程判断 状态计数”来支持同一线程反复加锁每次加锁state1每次解锁state-1直到状态归零锁才真正释放。从使用场景来看ReentrantLock的定位很清晰当业务需要灵活的加锁解锁时机、可中断获取锁、超时获取锁、公平排队、多条件队列这些能力时synchronized做不到或做得比较吃力就该考虑它了。我经常给同学打一个类比synchronized像“自动门禁”你进去它就锁上出来它自动打开你没得选。ReentrantLock像“手动闸机”你可以选择刷卡进、刷脸进、投币进甚至可以在闸机前排个公平的队还可以设定“我只等5秒等不到拉倒”。这种灵活性是靠底层的AQSAbstractQueuedSynchronizer支撑的。1.2 和synchronized的核心对比这两个锁的对比是面试常客也是理解ReentrantLock价值的入口。我从几个维度做了个对照表方便记忆但下面每一条我都会展开解释原理。对比维度synchronizedReentrantLock锁类型隐式内置锁自动获取和释放显式锁需要lock()/unlock()手动管理可重入性支持支持公平性非公平可公平可不公平构造器传入boolean中断响应不支持锁获取过程不可中断支持lockInterruptibly()超时获取不支持支持tryLock(timeout, unit)条件队列一个锁只有一个wait set用wait/notify一个锁可以创建多个Condition底层机制偏向锁→轻量级锁→重量级锁JDK 15后偏向锁废弃AQS CAS LockSupport性能经过多年优化大多数场景差距极小在高竞争下队列化更可控但优势已不明显很多文章爱说“ReentrantLock性能比synchronized好”这句话在JDK 6之后已经越来越不准确了。现代JVM对synchronized做了大量优化包括偏向锁、轻量级锁、锁消除、锁粗化在低竞争场景两者性能差距非常小。ReentrantLock的核心价值从来不只是性能而是它提供的那一组synchronized不具备的API能力。我个人使用时有一条经验如果你的临界区非常短且只需要纯粹的互斥优先用synchronized代码更简洁不容易出错如果需求里出现了“可中断、限时等待、多个条件等待、公平排队”字眼再考虑ReentrantLock。不要为了用而用技术选型永远服务于场景。1.3 源码结构鸟瞰打开ReentrantLock的源码可以看到它内部有三个关键角色ReentrantLock本身对外的门面实现Lock接口所有方法内部都委托给sync对象。Sync内部抽象类继承AbstractQueuedSynchronizerAQS实现了非公平的lock和可重入的tryAcquire/tryRelease这里存放的是公共逻辑。FairSync和NonfairSync两个内部类都继承Sync各自实现了acquire加锁逻辑。FairSync保证先来先得NonfairSync允许“插队”但源码里nonfairTryAcquire是放在Sync里复用的。这个设计很典型的“模板模式 策略模式”组合。模板方法在AQS里定好了acquire、acquireInterruptibly、release、acquireShared等而具体的“怎么尝试获取锁”由子类实现。ReentrantLock只需要选择用哪个策略类实例化即可。public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; public ReentrantLock() { sync new NonfairSync(); // 默认非公平 } public ReentrantLock(boolean fair) { sync fair ? new FairSync() : new NonfairSync(); } // 所有方法都是转发给sync public void lock() { sync.lock(); } }刚接触的同学容易把AQS和ReentrantLock画等号其实AQS是个更底层的工具类一套用来构建锁和同步器的框架。ReentrantLock只是这套框架最著名的应用实例。后面我写CountDownLatch、Semaphore、ReentrantReadWriteLock这些类时你会发现它们内部都是靠自己实现了AQS的某个逻辑。2. 加锁流程的源码拆解2.1 非公平锁的lock()为何会“插队”ReentrantLock最常用的构造是new ReentrantLock()内部是NonfairSync。先看它的lock实现static final class NonfairSync extends Sync { private static final long serialVersionUID 7316153563782823691L; final void lock() { // 第一枪直接CAS抢锁不排不队 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 抢不到才走AQS的标准加锁流程 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }注意这里的compareAndSetState(0, 1)它是一个CAS操作尝试把AQS的state字段从0改成1。如果成功说明锁正好无人持有当前线程直接拿到锁并被标记为持有者。这整个过程发生在进入AQS队列之前所以即使队列里已经有人在排队新来的线程也先试一把成功了就“插队”。这就是非公平锁的核心行为不保证等待时间最长的线程先拿到锁。但它有个重要前提插队只能发生在锁刚好被释放的瞬间。如果锁被持有新线程CAS失败它还是老老实实进入队列排队并不会直接甩开原有队列。acquire(1)是AQS的模板方法内部做了三件事public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }tryAcquire(arg)子类实现的具体抢锁逻辑addWaiter(Node.EXCLUSIVE)抢不到就封装成Node节点放进等待队列尾部acquireQueued(...)在队列里循环“尝试获取锁 / 挂起等待唤醒”直到拿到锁这里还隐藏一个细节selfInterrupt。acquireQueued返回true表示线程在排队等待的过程中被中断过这个方法会“补一次”中断标记把中断状态重新设置回去。这背后的逻辑是中断不应该丢失。线程在park挂起期间被中断外层代码可能需要通过中断状态感知这一事件所以内部必须把状态补偿回来。2.2 tryAcquire的“三步走”上面提到非公平锁的tryAcquire直接调用了nonfairTryAcquire这是放在Sync基类里的方法final boolean nonfairTryAcquire(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) // overflow throw new Error(Maximum lock count exceeded); setState(nextc); return true; } return false; }整个逻辑可以拆成三步走状态为0时尝试直接拿锁状态不为0时判断持有者是否是当前线程是则计数加一都不是则返回false由上层决定是否入队。这里有两个容易被忽略的点。第一为什么可重入时直接setState就行不需要CAS因为能走到这个分支意味着当前线程已经是锁的持有者而独占模式下不会有两个线程同时修改state竞争不可能发生所以普通赋值是安全的。这个推理依赖“独占锁持有者唯一”这一前提。第二重入计数溢出问题。nextc 0判断的是int溢出因为state是int类型理论上可以重入到溢出。如果真的发生了直接抛一个Error而不是异常因为这是程序无法恢复的错误状态。我遇到过有同学问“为什么这里要判断current getExclusiveOwnerThread()而不是直接拿线程名字做比较”因为线程对象引用比较才是准确的身份判断同名线程可能有很多但同一时刻持有锁的线程对象只有一个引用。2.3 入队与自旋的acquireQueued当nonfairTryAcquire返回falseAQS要把当前线程包装成Node加入等待队列。addWaiter里有个小优化先尝试通过尾节点快速入队如果尾节点为空说明队列还没初始化或者CAS失败才走enq自旋入队。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; }入队之后核心逻辑在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); } }这个循环的规矩很简单每个节点只关心它的前驱是不是head。如果前驱是head说明自己是队列的第一个排队者此时锁可能刚被释放就去tryAcquire抢一次。抢到了就“移除”自己原来的节点、成为新head整个队列前移一位。抢不到或者前驱根本不是head就要判断是否需要挂起。具体判断逻辑在shouldParkAfterFailedAcquire它会清理前驱中已取消的节点并把前驱的waitStatus改成SIGNAL目的是当前驱节点释放锁时有义务唤醒后一个节点。只有这个状态设置成功后当前线程才可以安全地park挂起。我个人读这段代码时最大的体会是CLH锁的“排队”不是一个线程一个位置地往前挪而是每个线程都坐在自己的椅子上等前面的兄弟叫醒自己等前面的兄弟拿到许可证走了自己再顶上。这种设计避免了大量线程同时自旋空转只有head节点的下一个节点才可能被唤醒。2.4 公平锁的lock()和tryAcquire差异FairSync的整体骨架和非公平锁一样差别只在tryAcquire上多了一个hasQueuedPredecessors()判断static final class FairSync extends Sync { 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()的官方语义是是否有线程比当前线程更早地排队等待获取锁。它有两个判断分支头节点不等于尾节点说明至少有一个有效节点在排队如果头节点有后继说明确实有人排在前面。那段看起来有点绕的代码本质上是在处理头尾节点重合、以及便利队列的边界情况。换句话说公平锁在state为0时不能直接抢必须先看队列里有没有人。只有队列为空或只有自己在等才有资格去CAS拿锁。有一个面试高频追问是“公平锁真的绝对公平吗”答案是否定的。公平锁只保证入队顺序的公平但线程调度、锁释放时机、以及tryAcquire本身的执行瞬间都存在微小差异。极端情况下就算你是队列里的第一个人只要在你被唤醒到执行CAS之间有人抢先释放并获取了锁比如刚释放锁的线程在unlock后又立刻lock你也得继续等下一轮。当然这种窗口非常短但理论上存在。公平锁会更慢吗大多数场景下是的。因为公平锁强行把并发抢锁变成了串行排队吞吐率通常低于非公平锁。但“更慢”换来的优势是线程唤醒的饥饿概率更低等待时间更可预期。像一些对响应时延敏感的系统公平锁反而更合适。3. 解锁流程与Condition机制3.1 unlock()的state递减和唤醒后继解锁的入口很简单public void unlock() { sync.release(1); }release是AQS的模板方法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的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; }这个方法有个容易忽略的细节先判断持有线程是否是当前线程不是就直接抛异常。所以如果你在非持有线程中调用了unlock会得到IllegalMonitorStateException。这也是和synchronized一个显著差异——synchronized的锁释放是JVM保证的不存在“别的线程释放”的问题ReentrantLock全靠显式调用很容易写错。state从1减到0时才真正释放锁并清空持有者如果大于0说明还有外层重入没退出锁继续保持持有状态。return free表示这一层release是否导致锁完全释放。只有完全释放了才需要去唤醒队列中的下一个等待者。unparkSuccessor是唤醒逻辑它会把head的waitStatus清0然后找下一个有效的后继节点并调用LockSupport.unpark。如果后继节点为空或已被取消就从尾节点反向遍历找到最靠近头部的有效节点。这里选择反向遍历是因为在并发入队场景中正向的next链可能还没来得及建立CAS入队是先连prev再连next的反向遍历更可靠。3.2 Condition是什么和多条件的价值Condition是ReentrantLock相比synchronized的一个大杀器。synchronized的等待通知是直接挂在对象监视器上的一个锁只能有一个等待集合用wait()/notify()/notifyAll()操作。而ReentrantLock通过newCondition()可以创建多个Condition对象每个Condition维护一条独立的等待队列。ReentrantLock lock new ReentrantLock(); Condition notFull lock.newCondition(); Condition notEmpty lock.newCondition(); public void put(E e) throws InterruptedException { lock.lock(); try { while (size capacity) { notFull.await(); } enqueue(e); notEmpty.signal(); } finally { lock.unlock(); } } public E take() throws InterruptedException { lock.lock(); try { while (size 0) { notEmpty.await(); } return dequeue(); } finally { lock.unlock(); } }这是经典的有界队列生产者消费者模型。用两个Condition可以精确控制消费者被唤醒只会是因为队列里有了数据生产者被唤醒只会是因为腾出了空间。如果换成synchronized的单一wait set只能用notifyAll唤醒所有线程被唤醒的线程还要一个个重新检查条件消耗无谓的竞争。多Condition的好处就像工地上的两个工头一个只喊“来货了”一个只喊“有空位了”工人各听各的不用理会不相关的声音。await()在释放锁底下的动作很讲究它不是简单的“释放锁等待”而是把当前线程封装成Condition队列的节点然后完全释放锁并把持有者置空最后park挂起。这里还涉及一个“保存重入次数再恢复”的细节因为锁可能被重入多次await时要一次全部释放并记录当时的state值唤醒后再重新恢复这个值。你可以在fullyRelease和fullyAcquire的源码里看到这一整套逻辑。signal()则负责把等待队列的头节点转移到AQS的同步队列尾部这样它就有资格重新参与锁的竞争了。如果转移时节点状态异常还会通过signalAll做兜底。3.3 锁中断与tryLock超时的底层支撑ReentrantLock的有两个方法面试经常被拿出来单独讲lockInterruptibly()和tryLock(timeout, TimeUnit)。lockInterruptibly()是“可中断的获取锁”。它在AQS里的模板是acquireInterruptiblypublic final void acquireInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) doAcquireInterruptibly(arg); }doAcquireInterruptibly和acquireQueued几乎一样唯一区别是线程在park期间被中断直接抛InterruptedException而不是记录中断标记后继续等。这意味着调用者可以用中断机制来打断锁获取过程。而普通的lock()遇到中断则会继续排队等拿到锁后才重新设置中断状态。tryLock(timeout, unit)走的是tryAcquireNanos本质是在等待加锁时调用了LockSupport.parkNanos做定时挂起超时时间到了还未拿到锁就返回false。注意这里有个细节超时返回false不等于锁就交出去了只是说你没在限定时间内拿到它拿到后还是要自己负责unlock。提醒使用tryLock(timeout)时拿到锁的线程必须在finally里unlock没拿到锁则绝对不能调用unlock否则会触发IllegalMonitorStateException。这个分支判断一定不能写错。4. 高频面试题拆解与回答思路4.1 ReentrantLock为什么是可重入的用它实现一个不可重入锁面试官问“什么是可重入”一般不会满足于“同一线程可以重复获取”这种表面答案。你要说清楚底层机制state字段既表示锁的持有状态也记录重入次数每次获取锁state加一每次释放state减一state归零锁才真正释放。如果想展现自己的理解深度可以说可重入的优点是避免同一线程在嵌套持锁时死锁但代价是维护重入计数带来额外的状态管理。对应的不可重入锁实现其实很简单一个标记位加一个持有线程就可以。public class NonReentrantLock { private boolean locked false; private Thread owner null; public synchronized void lock() { // 如果锁已被当前线程持有说明是重入直接拒绝 if (locked owner Thread.currentThread()) { throw new IllegalStateException(non reentrant); } // 自旋等待锁释放真实场景应该用LockSupport.park while (locked) { try { wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } locked true; owner Thread.currentThread(); } public synchronized void unlock() { if (owner ! Thread.currentThread()) { throw new IllegalMonitorStateException(); } locked false; owner null; notify(); } }这段代码会死锁吗如果外层方法调内层方法且都尝试lock就会抛异常而不是死锁这就是“不可重入”和“可重入”的直观区别。回答时对比一下面试官基本就能确定你真的理解了。4.2 公平锁与非公平锁的优缺点和适用场景这一题回答不能只背优缺点要落到源码行为上。非公平锁的lock()先直接CAS抢一次抢不到才进入AQS队列公平锁的tryAcquire多了hasQueuedPredecessors检查队列非空就乖乖排队。所以非公平锁的吞吐量通常更高因为减少了上下文切换、减少了线程挂起唤醒开销但存在“线程饥饿”风险长期等待的线程可能一直被新来的抢走锁。公平锁时延可预期适合对等待时间敏感的系统但代价是吞吐率下降。实战中的建议是默认用非公平锁只有当业务上无法接受“某些线程长时间等不到锁”的时才考虑公平锁。值得注意的是即使是非公平锁也不会完全饿死老线程——因为CAS抢锁只发生在锁释放的那个时间窗口如果没抢到还是要进入队尾排队的真正的饥饿概率其实较低。4.3 锁中断对竞争等待中的线程为何有效问这个问题的面试官通常想确认你对LockSupport和线程中断机制的理解。当线程执行park()挂起时如果另一个线程调用该线程的interrupt()park会立即返回并且中断标记被置为true。所以synchronized获取锁的过程中线程中断无效因为它不是基于LockSupport的主动park实现的而lockInterruptibly()的获取路径里线程在doAcquireInterruptibly中park中断能立刻唤醒它并抛出InterruptedException。这里还有一个值得背的细节Thread.interrupted()会清除中断状态而Thread.currentThread().isInterrupted()不会。AQS内部大量用Thread.interrupted()来判断并清除中断标记避免影响下一次排队。4.4 tryLock拿到锁没释放会怎么样直接回答锁不会被垃圾回收机制释放因为ReentrantLock的锁状态全在Java对象内部JVM不会像管理监视器锁那样自动清理。持锁线程不unlock其他线程要么一直阻塞在lock要么在tryLock上超时返回false最终可能引发死锁。这种情况在代码review里一定要重点盯。我自己的习惯是凡是lock和tryLock成对出现的方法一律要求写成这样boolean locked false; try { locked lock.tryLock(3, TimeUnit.SECONDS); if (!locked) { // 处理拿不到锁 return; } // 业务逻辑 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 按业务决定是否继续 } finally { if (locked) { lock.unlock(); } }注意locked变量初始化为false且在finally里判断只有当真正拿到锁才解锁。这句话看起来简单但实际项目里写错的人不少。4.5 Condition与synchronized的wait/notify相比强在哪回答时抓住三点。第一是精度多Condition可以把不同等待原因分开signal只唤醒对应等待集合的线程notifyAll做不到这种精确定位。第二是顺序Condition的等待队列和AQS同步队列是两条队列signal时只是把等待节点转移到同步队列不会像notify那样随机唤醒后需要重新竞争。第三是API丰富度Condition支持awaitUninterruptibly、awaitNanos、awaitUntil等更细的等待控制而这套机制在Object的wait体系中是没有的。如果被问到“signal和signalAll的区别”可以补充signal只唤醒等待队列的首个节点signalAll把所有节点都转移到同步队列。但不管用哪个被唤醒线程在重新获取锁之后都要重新检查等待条件因为有可能出现“唤醒后又因其他线程修改状态导致条件再次不满足”的情况这也是经典用while而不是if来判断等待条件的原因。4.6 AQS为什么用双向链表原因在哪这题不是直接问ReentrantLock但面试官特别喜欢顺带问。AQS同步队列选择双向链表而非单向链表本质是为了支持高效的取消操作和前驱状态检查。线程在等待过程中可能被中断或超时需要将自己从队列中摘除双向链表可以O(1)地修改前驱后继指针而acquireQueued中每个节点要检查前驱是否为head、设置前驱的状态为SIGNAL这都需要访问前驱节点单向链表的删除和状态传递会非常麻烦。另外还有一个细节入队时先设置prev再CAS设置尾节点出队时只改头节点双向链表让head后移和tail插入可以并发进行而不互斥。比如unparkSuccessor从tail反向遍历就是因为有些节点虽然入队但是next链还没完全建立反向用prev遍历保证一定能找到有效节点。5. 我在实际项目和面试准备中踩过的几个坑5.1 不按顺序加锁导致死锁很多人写多把锁的代码时没意识到加锁顺序的问题。我曾经参与过一个系统线程A先锁lock1再锁lock2线程B先锁lock2再锁lock1结果两个线程互相等着对方释放锁服务直接卡死。后来排查时用jstack看到一堆线程彼此等待才意识到是典型的“死锁”问题。建议是如果必须使用多把锁一定要全局约定一个固定的加锁顺序比如按业务对象ID从小到大并且用tryLock进行超时控制在拿不到后续锁时释放之前已获取的锁绝不能抱着“等一会儿就能拿到”的心态。5.2 Thread.sleep没释放锁有个刚毕业的同事和我们一起排查线上问题某线程持锁后执行了一段耗时流程他为了模拟延迟写了个Thread.sleep(5000)结果发现其他线程全部停摆。这就是对“sleep不释放锁只释放CPU”理解不到位。实际上锁的持有者sleep时锁依然被持有其他线程只能在队列里等待。这也是synchronized和ReentrantLock都有的行为不是ReentrantLock特有。真正需要做的是精简临界区代码尽量让锁持有时间短避免在持锁时做IO等待、网络请求等耗时操作。如果实在无法避免用一个更细粒度的锁或异步化方案往往更合理。5.3 忘记unlock导致线程泄露这个坑听起来很蠢但真的会反复出现。尤其是存在多个return分支、异常抛出路径时比较容易漏掉finally块。我的习惯是加锁后第一件事就是写finally并ctrl空格补全unlock然后再去写业务逻辑从结构上保证不会漏。另一个容易犯的错是unlock时没有判断“当前线程是否真的持锁”。比如用tryLock之后自己在finally里无条件unlock如果tryLock返回false这里就会抛IllegalMonitorStateException反而掩盖了本来“拿不到锁”的业务逻辑。这里我强烈建议用前面的“locked标志位”写法清晰且安全。5.4 锁的粒度与性能陷阱ReentrantLock用起来顺手但绝不应该把方法体全部锁住。某次我接手一个模块方法里有个耗时很长的外部HTTP调用被锁在临界区里QPS完全上不去。当时我先用日志统计了持锁时间分布发现大部分时间都花在HTTP等待上再把外部调用挪出临界区性能立刻提升了好几倍。做并发优化时我的排查流程一般是先看锁竞争是否真实存在用jstack采集BLOCKED/WAITING线程分布再看临界区内是否混入不必要的耗时操作最后才考虑要不要换成读写锁、分段锁或CAS替代。很多时候问题不在锁本身而在锁的范围。6. 面试中如何把这个主题讲出深度6.1 从“背概念”到“讲设计”现在面试Java并发岗位光背概念很难过关。比如被问到“ReentrantLock和synchronized区别”不只是列表格我会建议回答者这样组织先说使用层面的差异可中断、超时、公平性、多条件再说底层实现差异synchronized是JVM字节码指令级别的monitorenter/monitorexit以及偏向锁轻量级锁升级机制ReentrantLock是基于AQS的state加CAS加LockSupport最后落到“你会怎么选”以及“为什么默认非公平”。这样层层递进面试官能明显感觉到你不是背的。我有一位朋友之前面试某公司面试官直接打开源码让他讲NonfairSync的lock流程要求画出线程状态变化。他说当时如果能提前把compareAndSetState、addWaiter、acquireQueued、unparkSuccessor这条链路讲清楚再结合一个可重入状态的例子整个回答的完成度会高很多。6.2 源码阅读的实操方法很多人说看ReentrantLock源码看晕我觉得问题是缺少“带着目标去看”的思路。我给自己定的路径是先不看Condition只看lock和unlock的整条链路在NonfairSync.lock()处打断点用两个线程并发执行观察state的变化、Node节点的入队和head结点的推进不理解某个方法时用断点逐步执行看它改变了什么看完lock/unlock再去看Condition的await/signal调试AQS流程确实比较容易头晕因为多个线程交替执行。我之前调试时把断点条件设置成state为0的那一瞬间先看谁抢到了锁然后再模拟一个线程持锁、两个线程排队观察队列内部的Node状态流转。这样一步一步跑下来对代码的理解会比任何书都深刻。6.3 常见追问的应对原则ReentrantLock相关的追问往往是层层深入的。比如问到“可重入是怎么实现的”下面可能接着“state会不会有并发问题”“重入计数会不会溢出”。回答原则是如果能从源码里找依据就引用源码如果是推理就说明推理前提。例如state是int类型溢出需要大约21亿次重入实际操作中几乎不可能但源码里仍然做了溢出判断并把结果视为Error这体现库作者对异常防御的重视这种细节在面试里说出来的效果远胜于“int会溢出”这种简单答案。再比如被问“当前线程在await时会释放几次state”如果你知道fullyRelease会循环释放到state为0并记录原始值唤醒后用fullyAcquire恢复可以完整地解释“为什么await与持有的重入次数无关”。这也是面试官期待看到的深度远胜于说“await会释放锁”这种模糊描述。7. 说到最后一个我常用的二次封装技巧在结束前分享一个自己在项目中验证过多次的做法。为了降低业务代码里unlock遗漏的风险我通常会在团队工具类里提供一个“锁方法”的模板把lock和unlock的成对逻辑收敛在一个函数里public class LockTemplate { public static T T withLock(ReentrantLock lock, SupplierT action) { lock.lock(); try { return action.get(); } finally { lock.unlock(); } } public static void withLock(ReentrantLock lock, Runnable action) { lock.lock(); try { action.run(); } finally { lock.unlock(); } } }然后业务代码调用起来很干净LockTemplate.withLock(lock, () - { // 临界区 });这样的好处是unlock永远和lock出现在同一个函数里从代码结构上杜绝了忘记解锁的可能。当然它也有局限——比如锁的持有需跨越多个方法调用时这种方式就不合适了只能显式lock/unlock。但在一个方法内完成所有临界区操作的场景它是非常简单、安全的一种手段。ReentrantLock的内容远不止本篇这些后续我还会专门写Condition的深入解析、AQS共享模式分析以及如何用信号量模拟限流、如何用读写锁优化缓存等实战案例。面试也好日常写代码也好把锁的问题想透并发编程这块基本功就算有了一半。这篇先讲到这里接下来的几篇会继续把JUC这个系列补全。