
1. 为什么明明有 synchronized还需要 ReentrantLock很多刚接触 Java 并发的同学都会问同一个问题synchronized 用起来那么简单、那么顺手编译器还帮我们自动加锁解锁为什么还要冒出来一个 ReentrantLock说实话我最初学习的时候也有同样的困惑。直到在某个模拟高并发项目中真正遇到了 synchronized 解决不了的场景才意识到 Lock 体系的必要性。先明确一个基本事实synchronized 是 JVM 内置的监视器锁它不需要手动释放代码块执行完或者异常抛出时JVM 会自动帮我们释放锁。这个设计在 90% 的场景下都够用但它的能力边界也很清晰你只能拿到锁或者拿不到锁就只能一直傻等。假如一个线程持锁时间非常长其他线程就会无限期地阻塞下去没有“等待超时”这个概念也没有办法在等待锁的过程中响应中断信号。你想象一下一个线程卡在同步块门口外部想中止它结果发现根本停不下来只能干瞪眼。ReentrantLock 出现的意义就是为了补齐 synchronized 在灵活性上的短板。它实现了 java.util.concurrent.locks.Lock 接口提供了比 synchronized 更细粒度的控制能力可以尝试获取锁、可以限时等待、可以响应中断、可以公平排队、可以绑定多个条件队列。更重要的是它同样支持可重入性——一个线程已经持有锁的情况下可以再次获取同一把锁而不会死锁。这一点和 synchronized 的行为是一致的所以我们在设计嵌套锁结构时不用担心自己把自己锁死。我给一个非常直白的类比。synchronized 就像是高铁的自动闸机你刷票进去到了终点闸机自动打开你不用做任何额外操作。ReentrantLock 则像是一张需要手动刷卡、手动按下解锁按钮的门禁卡虽然多了一步操作但你可以决定什么时候开门、开门以后等多久、在门口排队的时候还能接个电话。为了这种额外的控制力我们付出的代价就是必须保证每次 lock 都有对应的 unlock。如果你是第一次接触 ReentrantLock那么这一篇我建议你重点看三块内容可重入机制到底是怎么实现的、公平锁和非公平锁的区别、Condition 条件队列怎么用它替代 wait/notify。把这三个点吃透你在并发编程上基本就能从“会用锁”进阶到“能设计锁策略”的阶段。2. ReentrantLock 的核心机制逐个拆解2.1 可重入性同一个线程为什么能反复加锁可重入性是 ReentrantLock 名字里最重要的一个词。它允许同一个线程对同一把锁重复执行 lock每执行一次锁的持有计数就加 1相应地每次 unlock 会让计数减 1直到计数归零锁才真正被释放其他线程才有机会获取它。这里面的关键点在于“持有计数”这个概念。ReentrantLock 内部并不是记录“哪个线程拿了锁”就完了而是同时记录“这个线程拿了几次”。核心实现里通过一个 volatile 修饰的状态变量来保存这个计数在线程尝试获取锁时会先判断当前状态值是否为 0。如果为 0说明锁没有被占用可以尝试 CAS 置为 1 并记录持有线程如果不为 0再判断持有线程是不是当前线程如果是就直接状态值加 1这个过程不需要 CAS 操作因为锁在自己手里改一个只属于当前线程的计数值没有竞争。为什么要设计成可重入最直接的原因是嵌套调用。比如一个类里有三个 synchronized 方法方法 A 内部调用了方法 B如果锁不可重入那么线程在进入方法 B 时就会发现自己被自己挡住了直接死锁。可重入性把这种“函数调用链上的锁冲突”完全屏蔽掉了编写业务代码时不需要刻意去拆解嵌套关系大大降低了心智负担。实际开发中我见过不少系统为了规避不可重入问题硬是把代码拆成两个锁或者用一步到位的大同步块结果反而增加了锁的持有时间性能和可维护性双双下降。ReentrantLock 在这一点上做得和 synchronized 完全一致适配起老代码来非常平滑。需要注意可重入计数也意味着 unlock 的次数必须和 lock 的次数完全匹配。你如果在一个方法里 lock 了两次却只 unlock 了一次那么锁的计数不会归零锁不会真正释放等待的线程会一直阻塞。这个问题在代码 review 的时候非常容易漏掉因为单看某个方法可能觉得没问题但整个调用链路走下来lock 和 unlock 的配对关系需要仔细数一遍。2.2 公平锁与非公平锁排队抢锁背后的性能博弈ReentrantLock 有一个构造参数叫 fair传入 true 表示公平锁默认是 false也就是非公平锁。公平锁的含义是锁的获取顺序严格按照线程等待的先后顺序来先到先得。非公平锁则允许新来的线程和已经在排队的线程一起竞争锁甚至可以“插队”成功。为什么要提供非公平锁因为在大多数场景下非公平锁的效率更高。新来的线程如果恰好能直接抢到锁就省去了进入等待队列、被唤醒、重新参与调度的整个流程减少了线程上下文切换的开销。打个比方公司只有一个卫生间大家都按顺序排队排队的每个人都要先“挂起”再被“叫醒”这个过程其实挺耗神的而如果你的面前刚好有空位直接推门进去就是了非公平锁允许这种“插队”。但非公平锁有一个明显的副作用极端情况下某个排队排了很久的线程可能一直抢不到锁出现饥饿现象。虽然实际发生的概率不高但理论上确实存在。从我的经验看公平锁适合那些对延迟敏感度要求一致、不能容忍某次请求长时间得不到处理的场景非公平锁更适合高吞吐量的场景比如某个模拟秒杀系统的库存扣减就适合非公平锁毕竟那条排队线程大概率立刻就能抢到一直让新线程“插队”反而能摊平平均等待时间。这里要给你一个实用的提醒公平锁内部是拿“是否有更早等待的线程”作为获取条件所以它的获取逻辑要比非公平锁多做一次查询性能自然低一些。网上很多文章会让读者“优先考虑公平锁”从人品角度没错但从工程角度我认为应该在确认业务真的需要公平语义时再开启 fairtrue。否则就默认用非公平锁性能和实现都更省心。2.3 AQS 与阻塞唤醒的实现基础ReentrantLock 的实现是建立在 AbstractQueuedSynchronizer简称 AQS也就是抽象队列同步器之上的。AQS 是 JUC 包里面最核心的基座除了 ReentrantLock像 CountDownLatch、Semaphore、ReentrantReadWriteLock 底层都依赖它。理解了 AQS你等于掌握了一把打开整个并发工具类的钥匙。AQS 的核心是一个 volatile 的同步状态变量加一个 FIFO 的等待队列。同步状态就是我在 2.1 节提到的计数等待队列则是把所有没抢到锁的线程组织成一个个节点按照到达顺序链接起来。线程获取锁的时候会尝试修改同步状态成功则直接进入临界区失败则把当前线程包装成节点挂到队列尾部然后阻塞自己等待前驱节点释放锁时被唤醒。阻塞和唤醒是怎么做到的呢底层依赖的是 LockSupport 这个工具类其中 park 方法负责让线程停止运行unpark 方法负责唤醒指定线程。之所以不用 Object 的 wait/notify是因为 LockSupport 不需要提前获取某个对象的监视器用起来更自由也更难出错。你在排查线上问题时如果看到线程堆栈停留在 LockSupport.park 的位置那基本就是在等锁或者等条件信号。我建议你顺着源码去读一遍 acquire 方法和 release 方法不用抠每行代码只要能看懂“状态变量变更 队列节点迁移 park/unpark 唤醒”这三个步骤后面学习其他 JUC 组件就会轻松很多。2.4 锁的可见性拿到锁不只是互斥锁能保证互斥是显性的但很多人忽略了锁的另一个关键作用保证临界区内的共享变量在多线程间可见。ReentrantLock 在锁的获取和释放过程中同样具备与 synchronized 一致的内存语义当一个线程释放锁时它写入的共享变量会先刷新到主内存而另一个线程获取到同一把锁时会把主内存中的最新值加载到自己的工作内存。这套机制靠的是 volatile 变量的读写屏障本质上是缓存一致性协议在工作。这带来一个实际结论你不要以为只有 volatile 修饰的变量才有可见性保障任何一把锁保护下的共享读写都天然具备可见性保障。所以我在设计共享数据时优先考虑的是“是否有一把统一的锁来覆盖所有读写路径”如果答案是肯定的不一定要额外加 volatile。当然这只是针对同一把锁覆盖的场景如果你有一些变量游离在锁之外被读写那 volatile 还是必须的。3. ReentrantLock 核心 API 实操手册3.1 lock 与 unlock 的基本使用姿势先看最标准的写法。以下代码在某个支付服务中用来保护账户余额字段private final ReentrantLock lock new ReentrantLock(); public void deductBalance(int amount) { lock.lock(); try { // 修改余额、记录流水、更新缓存 } finally { lock.unlock(); } }这段代码的核心就在 try-finallylock 放在 try 外面unlock 放在 finally 里面。为什么要这样因为 lock 之后只要进入 try 块无论后面的业务代码是正常执行完还是抛出异常中断finally 里的 unlock 一定会执行锁一定能被释放。如果你把 lock 写进 try 里面一旦 lock 本身抛出异常极有可能程序就跳到 catch 分支而根本没有持有锁再配一个 unlock 就会出问题。虽然 ReentrantLock 的 lock 方法正常情况下不抛受检异常但这种规范写法可以避免未来代码调整时埋雷。还需要注意的是锁对象应该声明为 final。你想想如果锁对象可以被重新赋值那么某个线程拿着旧锁对象进入临界区另一个线程拿着新锁对象也进入临界区两边各自为政互斥效果完全失效。这种错误我在代码评审时见过不止一次通常发生在对象被重新初始化或者框架做了代理包装的场景。声明成 final 是最简单的防御措施。3.2 tryLock拿不到锁就回头不硬等tryLock 是解决“无限等待”问题的第一件利器。它有两种重载不带参数的 tryLock 会立即尝试获取锁无论成功与否立刻返回布尔值带超时参数的 tryLock(long time, TimeUnit unit) 则会在指定时间内等待锁期间锁被释放会立刻返回超时了还没拿到就返回 false。if (lock.tryLock(2, TimeUnit.SECONDS)) { try { // 拿到了锁执行关键操作 } finally { lock.unlock(); } } else { // 拿不到锁的兜底逻辑记录日志、降级、返回繁忙提示 log.warn(获取锁超时当前请求繁忙); }这个 API 非常契合业务场景。比如某个查询接口需要加锁来保证数据一致性但允许接受一定程度的等待超过两秒还拿不到锁说明系统已经高度繁忙再等下去只会积累更多的排队线程不如直接失败让用户稍后重试。注意一点带超时的 tryLock 会响应线程的中断状态也就是说如果线程在等待过程中被中断它会抛出 InterruptedException 并且停止等待。如果你不想让等待过程被中断打断可以考虑用不带时间参数的 tryLock或者捕获中断异常后自行决定怎么处理。3.3 lockInterruptibly等待锁也能被叫停lockInterruptibly 从字面上理解就是“可中断地获取锁”。它的特殊之处在于如果线程当前没有持有锁并且在尝试获取锁的过程中收到了中断信号它会立即抛出 InterruptedException 退出等待状态。而普通的 lock 方法不会理会中断线程会继续阻塞在锁上直到拿到锁为止。什么场景会用到它一个典型的例子是线程池关闭时的优雅停机。假设线程池内有大量任务在排队等锁系统要停机了我们希望唤醒这些阻塞线程让他们感知到关闭信号并且尽快收尾。如果用普通 lock 方法线程会一直卡住无法响应主线程的关闭请求用 lockInterruptibly主线程调用 worker 的 interrupt 方法worker 就能从锁等待中被唤醒进入异常处理逻辑进而退出任务循环。这个特征在需要“可取消等待”的系统中非常重要建议你在设计任务调度组件时专门抽出时间验证这条路径。3.4 Condition用更优雅的条件等待代替 wait/notifyCondition 是 ReentrantLock 最大的亮点之一。它的作用类似于 synchronized 体系中的 wait/notify/notifyAll但强大得多。使用方式如下private final ReentrantLock lock new ReentrantLock(); private final Condition notEmpty lock.newCondition(); private final QueueString queue new LinkedList(); public String take() throws InterruptedException { lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); } return queue.poll(); } finally { lock.unlock(); } } public void put(String item) { lock.lock(); try { queue.offer(item); notEmpty.signal(); } finally { lock.unlock(); } }注意到一个关键点await 方法会释放当前持有的锁同时让线程进入阻塞状态当其他线程调用 signal 时等待的线程会被唤醒但不会立刻继续执行而是需要重新去竞争锁抢到锁之后才从 await 的位置继续往下走。整个过程和 wait/notify 的机制一致但 Condition 比 Object 的监视器方法更强的地方在于一把 ReentrantLock 可以创建多个 Condition从而实现不同条件分开等待。举个例子一个任务队列可以同时设置 notFull 和 notEmpty 两个条件生产者等待 notFull消费者等待 notEmpty。如果用 synchronized 加单一监视器来实现你只能用 notifyAll 唤醒所有线程失败的操作者被唤醒后发现条件还是不满足又得重新等待白白做一堆无用的上下文切换。而用了两个 Condition生产者只被 notFull 唤醒消费者只被 notEmpty 唤醒各司其职互不干扰。使用 Condition 时有一个常见的坑调用 await 之后忘记用 while 判断条件而是用了 if。业务上经常出现“多个生产者同时被唤醒其中一个抢到锁后把队列填满另一个生产者接着执行时才发现其实没有空位”的情况。所以务必要用 while 循环重新检查条件而不是用 if 判断一次就默认条件成立。4. ReentrantLock 和 synchronized 怎么选4.1 功能对比一目了然我把两者最核心的区别整理成一个表格方便你直接对照。对比维度synchronizedReentrantLock锁获取方式自动获取作用域内自动释放手动 lock必须手动 unlock可重入性支持支持中断响应不支持线程阻塞后无法唤醒lockInterruptibly 支持中断超时获取不支持tryLock 支持限时等待公平策略JVM 自行调度无法控制可指定公平锁或非公平锁条件队列每个对象只有一个 waitSet一个锁可创建多个 Condition底层实现JVM 指令层面monitor基于 AQS利用 state 和队列性能趋势偏向锁、轻量级锁优化后表现稳定争夺激烈时有明显吞吐优势使用难度极低几乎不会死锁需要严格的 unlock 纪律从这个表可以很清楚地看出synchronized 是“最简单可用的方案”ReentrantLock 是“更强大的手动控制方案”。4.2 不同场景下的选择判断我的选择原则是这样的如果只是需要保护一段数据不被并发写坏没有复杂的等待条件没有超时需求那么直接用 synchronized不需要任何纠结。因为现代的 JVM 对 synchronized 做了非常多的优化锁膨胀路径、偏向锁的撤销、轻量级锁的 CAS 自旋都让它在绝大多数场景下表现不错。而且它的代码可读性更高锁的生命周期一目了然几乎不可能出现锁忘记释放的问题。一旦你面临以下任何一种需求就要切换思考方向了。第一需要获取锁的超时控制比如不允许无限期等待第二需要等待锁的过程中响应中断比如优雅关机场景第三需要公平排队策略比如某些对公平性有硬性要求的业务第四需要多个条件队列做精细化的等待唤醒比如复杂的有界队列。碰到这四个信号直接锁定 ReentrantLock 就对了。另外还有一个偏工程化的建议。你如果长期维护同一个业务模块可以把锁策略封装成一个可配置项默认 using synchronized 版本再提供使用 ReentrantLock 的模式在压测环境里对比两者表现。这种对比不是纯粹的理论推演而是用实际数据来验证选择我认为这种做事方式比任何经验条款都可靠。4.3 从锁粒度角度看取舍锁的选择从来不只是锁本身的问题更是粒度设计的延伸。我在实践里发现很多人一遇到并发性能不达标就急着把 synchronized 换成 ReentrantLock换完之后发现收益微乎其微。原因往往出在锁粒度上——如果临界区本身过于庞大比如大段网络 IO、批量数据库操作都被锁包住那无论你用什么锁性能瓶颈都在持锁时间上而不是在锁的获取方式上。判断锁粒度是否合理的办法很简单看锁内有没有包含 IO 操作、远程调用、复杂的计算逻辑。凡是这类耗时操作都应该尽量移出临界区。真要保护状态一致性可以在锁内只做“计算新值 写入共享变量”这种轻量操作把耗时部分放到锁外去执行。这个原则对 ReentrantLock 同样成立。你换一把更快的锁不如把临界区缩小几倍后者的效果通常更显著。5. 常见问题与排查技巧实录5.1 锁没有释放线程卡成一片最典型的生产事故就是 lock 之后程序抛异常但异常没有被正确捕获unlock 没有执行所有需要这把锁的线程全部陷入阻塞。因为 ReentrantLock 不像 synchronized 那样由 JVM 自动处理解锁它把责任完全交给了开发者。即便程序崩溃了只要线程还在锁就可能一直被占用系统状态就像一辆刹车失灵的卡车越滑越快直到监控报警。排查办法很简单拿到线程堆栈看有没有大量线程停在同一个 LockSupport.park 或者 AbstractQueuedSynchronizer 的等待节点上然后找到持锁线程对应的业务代码段。解决办法就是严格遵循 try-finally 的写法。另外在测试环境专门用一个测试用例去触发临界区的异常路径验证锁是不是真的释放了这也是老项目改造时的常规操作。5.2 tryLock 超时判断错了业务状态很多人在 tryLock 拿到锁之后把业务处理逻辑直接放在 if 块里处理完在 finally 里 unlock。逻辑看似没问题但有个细节容易被忽略tryLock 返回 true 只代表“此刻拿到了锁”并不代表“业务数据是合法可操作的状态”。因为锁的语义是互斥不是状态校验。比如两条并发请求A 请求拿锁把库存扣到了 0B 请求拿锁时库存在锁外已经被读到了 0B 再继续扣就成了负数。所以正确做法是在锁内重新检查库存、剩余名额等前置条件该用 CAS 用 CAS该重读库存重读库存。5.3 公平锁与超时锁搭配的意外行为公平锁模式下tryLock 有一个容易踩坑的行为不带参数的 tryLock 在公平模式下是“非公平”的它不会遵守排队顺序会直接尝试抢占锁。也就是说即使前面有线程在排队只要锁恰好空闲后到的线程依然可能立刻拿到锁。很多人在写带公平锁的代码时误以为所有获取方式都是公平排队结果发现 tryLock 总能插队于是怀疑源码出了问题。其实这正是设计者有意为之的取舍为了支持某些场景下“宁可插队也要立刻执行”的需求。如果你希望 tryLock 也遵循公平策略就用带超时参数的版本那个版本会检查队列中是否有更早的等待者。5.4 await 后面忘了用 while 循环我在前面已经提到过这个坑但因为它太频繁了值得单独说一次。Condition 的 await 方法在被唤醒后并不会重新检查条件而是直接返回到原来的位置继续执行。这时如果共享队列的状态已经不再满足等待条件继续执行就会产生错误。标准写法永远是while (!condition) { condition.await(); }这一点和 wait 方法的官方推荐写法完全一致。即使现在只有一个生产者和一个消费者似乎不会出现“假唤醒”问题但我依然不建议你写 if 版本。任何代码都会演进一旦将来加了多个线程没准哪次唤醒就会出现意想不到的结果到时候排查起来要费不少功夫。5.5 锁和事务一起用时顺序反了在实际的业务系统里ReentrantLock 的临界区往往和方法级事务同时存在。最常见的错误是在事务提交之前释放锁。举个例子锁内修改了数据库记录然后把数据写进缓存最后在事务还没提交时 unlock紧接着另一个线程立刻拿到锁去读数据可能读到的是尚未提交、还没有回滚或已经修改过的中间状态。正确顺序应该是先让事务完成提交再释放锁保证持锁期间的所有状态变更对下一个拿到锁的线程是可见且合法的。这个顺序如果不注意排查起来特别隐蔽因为单看锁的释放时机看不出毛病一定要结合事务的边界来分析。6. 工程落地时需要记住的几件事6.1 锁对象的作用域与生命周期ReentrantLock 通常作为类的成员变量存在和它保护的共享数据放在一起。这样设计的好处很明显锁和数据有同样长的生命周期不会出现锁死锁或者锁被提前垃圾回收的问题。不要错误地把锁声明成一个方法内的局部变量——那样每个线程都有一把独立的锁互斥效果完全消失也不要放在 static 方法里随意共享除非你确定全局锁的粒度是你想要的效果。加锁时的顺序也要遵守纪律。如果代码里会同时获取多把锁必须确保所有线程都按相同顺序获取否则极易出现死锁。比如线程 A 先拿锁 1 再拿锁 2线程 B 先拿锁 2 再拿锁 1一旦时间点交错两边互相等着对方释放锁局面就僵死了。死锁的样子在线程堆栈里非常明显几个线程互相持有对方需要的锁形成循环等待。排查的时候把所有持锁线程的堆栈信息拉出来按锁的地址排序很容易看出问题。6.2 锁内不要做重活锁外尽量做校验我把这条经验分成两步来理解。第一步是缩小临界区范围把所有不需要同步保护的操作比如耗时计算、日志记录、外部接口调用都移出锁块。第二步是进入临界区后再做前置条件校验库存是否充足、状态机是否合法、版本号是否匹配。这两步合在一起才是标准的“先校验后加锁加锁后再业务判断”的完整路径。这里还有一个反直觉的点有时候把“校验”全部放在锁外让线程不拿锁就可以提前返回能大幅降低锁竞争。比如判断一个布尔开关的状态如果状态是关闭直接拒绝服务根本不用碰锁只有在开关打开需要操作共享数据时才进入锁保护。这个思路在网关限流、开关切换类功能上非常有效你可以试着在系统里找出几个类似的判断点性能提升效果会很明显。6.3 用读写锁进一步拆解共享数据访问ReentrantLock 是独占锁也就是无论读还是写都只能有一个线程进入。但实际业务中有大量场景是“多读少写”比如配置信息读取、基础数据缓存查询。这种场景下如果你还在用 ReentrantLock等于给所有读者也排了队白白浪费并发能力。ReentrantReadWriteLock 提供了更细的粒度读锁可以共享写锁独占。多个读者可以同时持有读锁但写者到来时所有读者必须退让写者拿到写锁后独占执行。这个模型完美贴合“读多写少”的业务。要注意的是读写锁在写线程等待时新的读线程会被阻塞以避免写线程饥饿。这个细节和 ReentrantLock 的公平策略有异曲同工之妙本质上都是“为了公平牺牲一点吞吐”。如果你的业务是复杂的数据结构操作比如并发安全的大 Map 或者列表可以优先考虑 ConcurrentHashMap 或者 CopyOnWriteArrayList 等更合适的容器而不是把锁直接写在业务代码里。6.4 JVM 层面的优化不是“不用锁”的理由现在很多资料喜欢强调锁升级、锁消除、锁粗化这些 JVM 优化给人一种“用 synchronized 就好因为 JVM 会帮我优化”的感觉。这话对但也不全对。JVM 的优化只能改善锁的获取和释放路径比如消除不必要的锁竞争、把相邻的同步块合并但它无法改变锁竞争本身的性质。如果多个线程同时激烈地争夺同一把锁锁竞争依然会造成上下文切换和缓存失效JVM 可没法让它变成无锁。所以看待 ReentrantLock 的正确姿势不是“替代品”而是“另一个选项”。对于绝大多数小型、短临界区、低竞争场景用 synchronized 就够对于大并发、高竞争、需要超时中断或者多条件队列的场景就应该主动拥抱 ReentrantLock。关键是要真正理解锁的工作机制而不是死记硬背某个“金科玉律”。6.5 压测中的锁竞争指标怎么看我习惯在压测时关注两个指标。第一个是吞吐量也就是单位时间能处理多少请求第二个是等待锁的平均时间这个时间越长说明锁竞争越严重。如果等待时间很长即使吞吐量还行系统也会随着线程增多而变慢因为大量线程都在“等锁-被唤醒-再等锁”的循环里空转。定位这种问题可以用一些线程分析工具定时抓取线程堆栈统计有多少线程处于 BLOCKED 或者 WAITING 状态。另一个可行的做法是开启 JFR 之类的诊断功能它会记录锁竞争事件配合分析平台能看到哪把锁最热。这些工具的使用并不复杂难的是你自己对锁结构的把握。比如同样一个并发控制器可能影响性能的不是锁本身而是额外加了太多的日志输出、异常捕获和条件分支。我经常建议别人先跑一遍没加锁的基线版本再跑一遍加锁版本两者对比后的差值才是“锁带来的成本”。知道这个差值你就知道是否值得为锁功能做更多的优化。在我个人参与的多个并发项目里凡是把锁真正用好的人都是先把自己负责的临界区画清楚哪几个变量是共享可变状态哪些方法可能并发访问这些状态加锁的拦截点设置在哪里。这个“先画图、后写锁”的习惯比任何锁 API 的熟练度都重要。ReentrantLock 只是工具真正的核心是把并发访问模型设计明白。最后再说一个小技巧。如果你在某段代码里多次对同一把锁执行 lock 操作又怕自己忘了配平 unlock可以在一个独立的方法里把“加锁-执行业务-解锁”整体封装起来比如写一个带回调的执行函数。这样调用方只需要传业务逻辑锁的获取和释放都在框架层统一处理既安全又清爽。实际用下来的体验是锁的成对逻辑越集中代码出问题的概率就越小。