资讯详情

ReentrantLock深入解析:从API到AQS原理与实战

📅 2026/10/11 8:00:05 | 华诺云谱 👁 阅读
ReentrantLock深入解析:从API到AQS原理与实战
在Java并发编程这条路上ReentrantLock算是绕不开的一个名字。我早年在做交易系统的时候遇到过大量并发扣减、排队等待、超时降级的场景synchronized 用起来总有点使不上劲的感觉真正把 Thread 阻塞、唤醒、超时、中断这些细节玩明白就是从啃透 ReentrantLock 和它背后的 AQS 开始的。这几年不管是自己写代码还是帮别人排查线上问题甚至面试候选人ReentrantLock 都是出现频率极高的知识点。这篇文章我打算从日常 API 用法讲到底层 AQS 原理再到我在实战里踩过的坑和整理的面试题完整过一遍希望给正在学并发编程或者准备 Java 面试的朋友一份能直接照着用的参考。1. ReentrantLock是什么从synchronized的局限说起1.1 synchronized的三个硬伤要说 ReentrantLock得先弄明白它到底解决了什么痛点。synchronized 是 Java 内置锁用起来确实省事方法加个关键字JVM 自动帮你加锁解锁。但在复杂的并发场景下它有几个先天不足。第一拿到锁的过程无法响应中断。一个线程如果阻塞在 synchronized 上等锁别的线程调用它的 interrupt() 方法它也不会醒来只能干等。这在实现任务取消的时候非常难受线程池 shutdownNow() 想通过中断来停掉正在等锁的任务结果根本停不下来。第二没有超时机制。synchronized 会一直等下去直到拿到锁为止。如果持锁线程因为某种原因迟迟不释放其他线程就只能无限期阻塞系统请求积压最终可能雪崩。实际线上环境里你特别需要一个最多等 3 秒等不到就放弃走降级的能力。第三公平性不可控。synchronized 本质上是非公平锁新来的线程是可能插队的。虽然大多数场景无所谓插队但某些任务调度系统要求先来先服务这时候 synchronized 就没法满足了。synchronized 还有一个不算硬伤但确实不够灵活的问题它只有一个隐式条件队列wait/notify 没法做到按不同条件分类等待。想实现一个生产者消费者模型一个锁上既要等队列不满又要等队列不空用 synchronized 只能全部唤醒再自己 check效率不高。ReentrantLock 就是冲着这些痛点来的。1.2 ReentrantLock的核心特性总览ReentrantLock 是 java.util.concurrent 包下 Lock 接口的一个实现类在 JUC 里地位非常高。我一般直接叫它可重入互斥锁它最重要的几个特性可以先用一张表拉出来特性说明可重入同一个线程可以重复获取同一把锁内部用计数记录重入次数可中断支持 lockInterruptibly()等待锁的过程中可以响应中断可超时支持 tryLock(timeout)最多等待指定时间拿不到就放弃公平性可选构造时可指定公平锁或非公平锁多条件队列一个锁可以创建多个 Condition按不同条件分组等待基于 AQS 实现底层由 AbstractQueuedSynchronizer 提供同步状态和线程排队能力后面几章我会把每个特性展开讲尤其是 AQS 那部分是整个 JUC 的基石理解了它再看 Semaphore、CountDownLatch、ReentrantReadWriteLock 都会豁然开朗。2. ReentrantLock核心API用法详解附代码2.1 最基础却最容易翻车的lock/unlockReentrantLock 最经典的用法就是 lock() 加锁然后在 finally 里 unlock() 释放锁。直接看代码import java.util.concurrent.locks.ReentrantLock; public class Counter { private final ReentrantLock lock new ReentrantLock(); private int count 0; public void increment() { lock.lock(); try { count; // 这里放真正的业务逻辑 } finally { lock.unlock(); } } }这段代码看起来简单但我要强调三个点。第一lock() 和 unlock() 必须是成对出现的而且释放动作一定要放在 finally 里。synchronized 是 JVM 自动释放锁哪怕方法抛异常也没关系。ReentrantLock 可不惯着你如果业务代码在 try 里抛了异常后面的 unlock() 不会执行锁就泄漏了。我见过一次线上事故就是有人在一个遍历数据的循环里加了锁结果中间有个数据抛了异常锁没释放整个服务被拖垮线程全部堆积在等锁上。排查到最后就是一句忘了加 finally的问题。第二lock() 之后到 try 之前的代码越少越好。最稳的写法是lock.lock()下一行直接try {中间不要插任何可能出错的代码。有些人喜欢在加锁和 try 之间写日志、做判断一旦这里抛异常锁照样泄漏。第三锁内的代码要尽量短。ReentrantLock 不是银弹在持锁状态下做 IO、网络调用、远程 RPC都属于大忌。你锁的时间越久其他线程等待的时间就越长。我一般的原则是临界区只放必须保护的共享变量操作能挪出去的都挪出去。2.2 tryLock与lockInterruptibly让等待有弹性如果说 lock() 是把等待做成了无限期那 tryLock 和 lockInterruptibly 就是把等待变成了有弹性。tryLock()非阻塞尝试public boolean tryLockDemo() { if (lock.tryLock()) { try { // 拿到了锁执行临界区 return true; } finally { lock.unlock(); } } // 没拿到锁直接返回 false return false; }tryLock() 不带参数时立即返回不等待。拿得到就返回 true拿不到就返回 false你可以走降级逻辑、直接拒绝请求、或者记录日志等下轮补偿。这种模式适合能抢到就处理抢不到就算了的场景比如缓存刷新、定时任务里更新某个状态错过一次下次再刷就是了没必要阻塞线程。tryLock(long time, TimeUnit unit)限时等待import java.util.concurrent.TimeUnit; public boolean tryLockWithTimeout() throws InterruptedException { boolean acquired lock.tryLock(3, TimeUnit.SECONDS); if (!acquired) { // 3秒内没拿到锁返回失败调用方决定是否降级 return false; } try { // 拿到锁执行临界区 return true; } finally { lock.unlock(); } }这个版本会等待指定的时间期间拿不到锁就返回 false。我刚工作那会儿写订单状态流转就是用的这种方式最多等 2 秒拿订单锁拿不到直接告诉用户操作繁忙请稍后再试而不是让请求一直挂着。lockInterruptibly()可中断的锁获取public void lockInterruptiblyDemo() throws InterruptedException { lock.lockInterruptibly(); // 如果等待过程中线程被 interrupt会抛出 InterruptedException try { // 拿到锁执行临界区 } finally { lock.unlock(); } }lockInterruptibly() 和 lock() 最大的区别在于当线程在等待锁的过程中被 interrupt()lock() 不会理睬线程继续等待lockInterruptibly() 会立刻抛出 InterruptedException并停止等待。这样外部就能通过中断来取消一个正在等锁的任务对实现可取消任务特别重要。这里有个细节InterruptedException 抛出后线程的中断标记会被清除。如果你在 catch 里不做处理下一次再检查中断状态就会丢失信息。我习惯在 catch 块里重新设置中断标记Thread.currentThread().interrupt()把中断状态传递下去。三种获取方式的适用场景我做成了表格方法是否阻塞是否响应中断适用场景lock()阻塞直到成功否必须拿到锁才能继续不计较等待tryLock()不阻塞立即返回是拿不到就走降级/直接放弃tryLock(timeout, unit)阻塞但有时间上限是有最大等待时限避免无限阻塞lockInterruptibly()阻塞直到成功或被中断是可取消任务、优雅停机2.3 公平锁与非公平锁选错有代价ReentrantLock 的构造方法里可以传一个 booleantrue 表示公平锁false 表示非公平锁默认是非公平锁。ReentrantLock fairLock new ReentrantLock(true); // 公平锁 ReentrantLock unfairLock new ReentrantLock(false); // 非公平锁公平锁的意思是线程按照到达顺序获取锁先到先得大家排队来。非公平锁的意思是新来的线程会先尝试直接抢锁抢得到就插队成功抢不到才排到队尾。性能上非公平锁通常吞吐量更高。原因是当持锁线程释放锁时唤醒排队中的线程需要上下文切换有一定开销。如果恰好有一个新线程此时来请求锁它可以直接拿到锁省掉了唤醒和切换的成本。而且刚释放锁的线程大概率还占据着 CPU 时间片新线程顺手就接上了CPU 利用率更高。非公平锁的代价是队列里排队的线程可能被反复插队理论上存在饥饿风险不过实际发生的概率非常低。我之前在一个任务队列系统里用过公平锁体验是公平性能保证相对公平但整体吞吐量确实下降了毕竟多了一些队列检查和唤醒成本。绝大多数业务场景默认的非公平锁就够了。只有像多个消费者按请求顺序抢占同一批任务这种业务要求明确的场景才值得上公平锁。2.4 Condition条件变量多队列等待的正确姿势这部分我强烈建议你花时间搞懂因为它是 ReentrantLock 相对 synchronized 的巨大优势。synchronized 配合 wait/notify 只有一个等待队列你没法区分等队列满的人和等队列空的人。ReentrantLock 可以创建多个 Condition相当于把等待的人按不同条件分开排队。下面是一个有界缓冲区的经典实现用两个 Condition 分别管理队列不满和队列不空public class BoundedBufferT { private final Object[] items; private int putIndex; private int takeIndex; private int count; private final ReentrantLock lock new ReentrantLock(); private final Condition notFull lock.newCondition(); private final Condition notEmpty lock.newCondition(); public BoundedBuffer(int capacity) { items new Object[capacity]; } public void put(T value) throws InterruptedException { lock.lock(); try { while (count items.length) { notFull.await(); // 队列满了生产者在这排队 } items[putIndex] value; putIndex (putIndex 1) % items.length; count; notEmpty.signal(); // 通知消费者有东西可以取了 } finally { lock.unlock(); } } SuppressWarnings(unchecked) public T take() throws InterruptedException { lock.lock(); try { while (count 0) { notEmpty.await(); // 队列空了消费者在这排队 } Object value items[takeIndex]; items[takeIndex] null; takeIndex (takeIndex 1) % items.length; count--; notFull.signal(); // 通知生产者有位置可以放了 } finally { lock.unlock(); } return (T) value; } }注意几个容易踩的坑await() 和 signal() 必须在持有锁的前提下调用否则抛 IllegalMonitorStateException。await() 会让出锁进入 Condition 自己的等待队列。被 signal() 唤醒后线程并不能立刻继续执行它要先重新竞争到锁从 await() 返回后再往下走。一定要用 while 不用 if来重新检查条件。因为可能存在伪唤醒spurious wakeup而且就算不是伪唤醒多线程竞争下也可能出现信号丢失。while 循环能保证醒来后条件确实满足才继续走。3. 从AQS看ReentrantLock的底层实现3.1 AQS是什么为什么所有同步器都离不开它AQS 全称 AbstractQueuedSynchronizer是 java.util.concurrent 包的核心基础设施。后面那一堆同步工具——ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock——底层全都是基于 AQS 实现的。ReentrantLock 内部有一个继承了 AQS 的 Sync 类又分 FairSync 和 NonfairSync 两个子类分别对应公平锁和非公平锁的逻辑。所以 AQS 就是 ReentrantLock 的心脏你只要把 AQS 看明白了ReentrantLock 就通了八成。AQS 的核心就两块东西一个volatile int state表示同步状态。一个FIFO 的等待队列内部结构是双向链表存放获取锁失败的线程。3.2 state变量与CLH队列银行取号排队模型我把 AQS 的设计用一个生活场景类比一下很好理解。想象只有一个柜台的银行柜台就是锁。第一个到的人发现柜台没人直接办业务这就是state 0时直接 CAS 获取锁。如果柜台被占用后来的人不能都挤在柜台前他们要拿号排队这就是把线程包装成 Node 节点进入 CLH 队列。CLH 队列是双向链表每个节点至少保存这几个字段thread等待的线程prev / next前驱和后继节点waitStatus等待状态比如被取消、需要唤醒等为什么用volatile int state而不是普通的 int因为 state 要保证跨线程可见性而且获取锁时需要用 CASCompare And Swap原子地把 state 从 0 改成 1。CAS 是硬件层面的原子操作多个线程同时改 state 也不会出现数据错乱。3.3 加锁流程完整链路以非公平锁为例调用 lock() 时核心逻辑其实就一个方法public final void acquire(int arg) { if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } }这个方法信息量很大我拆开讲。第一步tryAcquire(arg)尝试直接获取锁。非公平锁的做法是final boolean nonfairTryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); if (c 0) { // 用 CAS 把 state 从 0 改成 1成功说明抢到了锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current getExclusiveOwnerThread()) { // 锁已经被自己持有这是重入 int nextc c acquires; setState(nextc); return true; } return false; }注意非公平锁在state 0时不管队列里有没有人排队上来就先 CAS 抢一下这就是插队的来源。第二步如果 tryAcquire 失败进入addWaiter(Node.EXCLUSIVE)把当前线程包装成一个节点通过 CAS 追加到 CLH 队列尾部。这一步保证了多个线程同时入队时不会互相覆盖。第三步acquireQueued()让线程在队列里进入循环。每个节点会不停判断我的前驱是不是 head如果是说明轮到我了再尝试一次获取锁如果获取成功就把自己设为新的 head。如果前驱不是 head或者抢锁失败就把前驱节点的 waitStatus 改成 SIGNAL然后挂起park等待被唤醒。第四步如果整个过程被中断acquire 返回后会执行selfInterrupt()补上中断标记。整个流程就像一个排队的客户先试试柜台空没空没空就取号排队排队过程中反复确认是不是轮到自己轮到就冲上去办理没轮到就睡觉等前一个人办完叫醒自己。3.4 解锁流程完整链路解锁的核心方法是 releasepublic final boolean release(int arg) { if (tryRelease(arg)) { Node h head; if (h ! null h.waitStatus ! 0) { unparkSuccessor(h); } return true; } return false; }tryRelease 的实现逻辑是把 state 减 1protected 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才说明锁真正被释放了这时把持有者线程置为 null并唤醒 head 的下一个有效线程unparkSuccessor。被唤醒的线程会继续在 acquireQueued 的循环里尝试抢锁。这里有一个面试常考的小细节为什么释放锁不需要 CAS因为能执行 release 的线程必然是当前持锁线程state 的修改是单线程操作不存在并发写竞争所以直接 setState 就够了。而获取锁时 state 可能从 0 变 1这是多线程竞争改同一个变量必须用 CAS 保证原子性。3.5 可重入的核心原理重入的实现原理在 tryAcquire 里已经很明显了当state ! 0时如果当前线程等于getExclusiveOwnerThread()说明这把锁本来就是自己持有的那就直接把 state 加 1返回成功。释放时每调一次 unlockstate 减 1直到减到 0 才算真正释放。这个设计很像一个计数器你重入了几次就要释放几次。如果一个线程重入 3 次只释放了 2 次那 state 还剩下 1其他线程还是拿不到锁——这就是很多锁泄漏事故的根源。我在实际中见过有人在一个方法里嵌套调用了多个加锁方法释放次数没对上结果锁被焊死了排查的时候才发现重入计数没清零。可重入这个特性还有个实际价值它允许你在递归方法里安全地加锁不会出现自己跟自己死锁的问题。下一章的实战案例我会演示。4. ReentrantLock vs synchronized实际项目中该怎么选4.1 功能对比一张表看清差异很多新手容易纠结一个问题既然 synchronized 那么简单ReentrantLock 功能又多那是不是无脑用 ReentrantLock 就行了其实不是。两者各有优势看场景。维度synchronizedReentrantLock释放方式JVM 自动释放必须手动 unlock可重入支持支持可中断不支持支持超时获取不支持支持公平性默认非公平不可配可配置公平/非公平多条件队列只有 wait/notify多个 Condition实现机制对象监视器 monitorAQSstate CAS 队列异常时的锁状态自动释放必须 finally 释放否则泄漏从这张表能看出来ReentrantLock 的功能确实更全面但它也把正确释放锁的责任完全交给了开发者。synchronized 写起来更无脑出错的概率天然更低。4.2 性能对比JDK版本带来的变数在 JDK 1.5 的时代synchronized 的性能确实被 ReentrantLock 吊打所以很多老代码里都用 ReentrantLock。但 JDK 1.6 之后JVM 对 synchronized 做了大量优化引入了偏向锁、轻量级锁、锁消除、自适应自旋等机制。在大多数场景下synchronized 和 ReentrantLock 的性能差距已经可以忽略不计。我自己做一些简单的性能压测时这两者在单线程或低竞争场景下几乎没差别。高竞争场景下ReentrantLock 因为基于 AQS 和 CAS在某些时候表现会好一点但也没有绝对优势。所以想靠选择锁来显著提升性能基本是不现实的更重要的还是缩小临界区、减少锁竞争。4.3 我的选型建议我写代码的时候会遵循这么一条原则如果只需要简单的互斥没有可中断、可超时、多条件队列这些需求直接 synchronized。如果需要可中断、可超时获取锁或者需要公平性控制选 ReentrantLock。如果同时需要多个条件队列比如有界队列的生产者消费者模型优先 ReentrantLock Condition。如果读多写少可以考虑ReentrantReadWriteLock。如果单机内能解决优先考虑并发容器、原子类、无锁方案加锁始终是最后手段。我见过太多人一上来就 ReentrantLock把代码写得复杂最后忘了释放锁或者 Condition 用错反而得不偿失。工具是为人服务的选简单的能解决问题就不要上来就上复杂的。5. 实战案例库存扣减与生产者消费者模型5.1 单机库存扣减用tryLock实现快速失败高并发下扣减库存是经典的并发控制场景。用 JVM 本地锁只能保证单节点内安全跨节点的场景得靠数据库乐观锁或分布式锁这个后面说。这里先用 ReentrantLock 演示单机版本的正确姿势import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class InventoryService { private int stock; private final ReentrantLock lock new ReentrantLock(); public InventoryService(int stock) { this.stock stock; } public boolean deduct(int quantity) { boolean acquired; try { acquired lock.tryLock(100, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } if (!acquired) { // 100ms 内没拿到锁说明竞争激烈直接快速失败 return false; } try { if (stock quantity) { // 模拟耗时操作 stock - quantity; return true; } return false; } finally { lock.unlock(); } } public int getStock() { return stock; } }为什么这里用 tryLock(100, MILLISECONDS) 而不是 lock()因为在秒杀这类场景里如果并发太高请求与其一直在阻塞等待锁不如快速失败给用户返回太火爆了请稍后再试。锁竞争激烈时快速失败的体验远好于无限等待。还要注意 InterruptedException 的处理。我在 catch 里做了两件事恢复中断标记返回 false。如果不恢复标记上层线程再检查中断状态时就感知不到这次中断了。这个小细节很容易被忽略。真实项目里单机锁还有个大前提它只能管住当前 JVM 进程内的线程。如果服务是多节点部署请求分散在不同的机器上每台机器各自扣减自己的本地库存照样会超卖。这种场景要么引入 Redis 分布式锁要么用数据库乐观锁扣减。面试的时候如果被问到库存扣减怎么做能主动把单机和分布式的分界线讲清楚很加分。5.2 有界阻塞队列Condition的经典战场回到第 2.4 节的 BoundedBuffer配一个完整的运行示例。public class ProducerConsumerDemo { private static final BoundedBufferInteger buffer new BoundedBuffer(10); public static void main(String[] args) { Thread producer new Thread(() - { for (int i 0; ; i) { try { buffer.put(i); System.out.println(生产: i); Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }); Thread consumer new Thread(() - { while (true) { try { int value buffer.take(); System.out.println(消费: value); Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } }); producer.start(); consumer.start(); } }这个模型的妙处在于当队列满时生产者会阻塞在 notFull.await()消费者取走一个后调用 notFull.signal()生产者才醒来继续生产。当队列空时消费者阻塞在 notEmpty.await()生产者放入一个后调用 notEmpty.signal()消费者才醒来取数据。如果用 synchronized 的 wait/notify 来实现只能 notifyAll 把所有人都叫醒然后每个人自己重新 check 条件大量无谓唤醒。Condition 把同条件的人分组管理唤醒目标更精准效率高代码语义也更清晰。这就是 ReentrantLock 在这类场景里最大的价值。5.3 验证可重入性递归加锁演示可重入这个概念用递归方法验证最直观public class ReentrantDemo { private final ReentrantLock lock new ReentrantLock(); public void recursive(int depth) { lock.lock(); try { System.out.println(当前深度: depth , 锁重入次数: lock.getHoldCount()); if (depth 0) { recursive(depth - 1); } } finally { lock.unlock(); } } public static void main(String[] args) { ReentrantDemo demo new ReentrantDemo(); demo.recursive(3); } }运行后你会发现同一个线程可以连续 4 次拿到同一把锁getHoldCount() 从 1 递增到 4最后又递减到 0。如果 ReentrantLock 不支持可重入第二次递归在 lock() 时就会把自己阻塞住形成死锁。这个特性保证了递归、嵌套调用中的加锁是安全的也让锁的语义更加贴合真实业务逻辑。6. 避坑指南与高频面试题整理6.1 我踩过的坑锁泄漏与死锁复盘写了这么多年并发代码ReentrantLock 相关的坑我基本都踩过一遍。挑几个最典型的分享。坑一lock 和 try 之间插了逻辑有次我 review 代码看到有人这么写lock.lock(); if (config null) { throw new RuntimeException(config is null); } try { // ... } finally { lock.unlock(); }如果 config 为 null异常抛出时锁还没释放后面所有线程全被堵死。正确做法是 lock 之后立刻进 try。坑二重入次数和释放次数不配对一个人写了一个加锁方法内部又调用了另一个加锁方法但只在最外层释放了一次锁。ReentrantLock 重入了两次却只释放一次state 永远停在 1其他线程永远拿不到锁。这种问题特别隐蔽因为代码看起来逻辑都对不打印 getHoldCount() 根本发现不了。坑三多把锁的加锁顺序不一致导致死锁线程 A 先拿锁 1 再拿锁 2线程 B 先拿锁 2 再拿锁 1两边各持有一把锁互不相让就死锁了。ReentrantLock 提供了 tryLock 可以在拿不到第二把锁时释放第一把锁并重试但最根本的解决办法是全局约定锁的获取顺序从源头上避免交叉。坑四Condition 的 await 用 if 判断前面已经强调过必须用 while。如果只用 if线程被唤醒后不会重新检查条件可能拿到的数据是过期甚至错误的。这个属于并发编程里的基础习惯问题但新手特别容易犯。这些坑我总结成一个速查表方便你收藏坑点原因正确做法忘记 finally 解锁异常路径中断lock 后立即 try-finally重入次数和释放次数不匹配嵌套加锁没注意配对用 getHoldCount() 排查多把锁顺序不一致设计之初没约定全局固定加锁顺序await 用 if 不用 while忽略伪唤醒和竞争统一用 while 重新检查条件持锁做 IO/远程调用锁粒度失控临界区只放共享变量操作中断未恢复标记捕获异常后忘了catch 中重新 interrupt()6.2 高频面试题从原理到扩展ReentrantLock 在面试八股文里出现的频率高得惊人我整理过一份高频问题清单基本就是这些。Q1ReentrantLock 和 synchronized 的区别从功能上对比释放方式、可中断、超时、公平性、多条件队列这几条逐个说再补一句JDK 6 之后性能差距不大。如果能主动提到底层一个是 monitor 一个是 AQS印象分会好很多。Q2AQS 的原理是什么先讲两个核心volatile int state CLH FIFO 队列。再讲加锁流程tryAcquire 先尝试获取失败后 addWaiter 入队acquireQueued 里自旋/阻塞等待head 唤醒后继节点。模板方法设计是 AQS 的精髓子类只需要实现 tryAcquire/tryRelease 等方法。Q3ReentrantLock 默认为什么是非公平锁因为非公平锁吞吐量更高。它避免了唤醒队列中线程的上下文切换开销新线程直接抢锁能更充分地利用 CPU 时间片。公平锁虽然更公平但整体性能会下降。Q4可重入是怎么实现的state 计数 持有者线程判断。当 state 0 且当前线程就是持有者线程时直接加 1 而不阻塞。释放时同样减 1减到 0 才真正释放锁。Q5lockInterruptibly 和 synchronized 在中断上的区别synchronized 等待锁时不能响应中断lockInterruptibly 可以。如果一个线程在等待 ReentrantLock 时被 interrupt()lockInterruptibly 会抛 InterruptedException。Q6Condition 和 wait/notify 的区别Condition 需要和 Lock 配合使用一个 Lock 可以创建多个 Condition实现分组等待、定向唤醒。而 wait/notify 只有一个隐式条件队列唤醒也是全体的。Condition 还支持 awaitUninterruptibly、awaitNanos 等更精细的方法。Q7为什么释放锁不需要 CAS只有持锁线程才能执行 release这时 state 的修改是单线程操作没有并发竞争直接 setState 就行。而获取锁时是多个线程竞争同一个 state必须用 CAS 保证原子性。6.3 最后的建议学 ReentrantLock 的时候不要只背 API 和面试答案最好自己动手写几个 Demo 验证一下。比如写一个生产者消费者、写一个 tryLock 快速失败、再尝试打印 getHoldCount 观察重入次数。代码跑起来之后你对 AQS 的印象会深刻得多。再往后可以去看一下 JUC 包里 Semaphore、CountDownLatch 的源码你会发现它们都是同一个套路只是 tryAcquire/tryRelease 的逻辑不同。理解了这一层Java 并发编程的很多谜团就解开了。实际上每次做并发相关的项目我都会先问一句非要用锁吗能用并发容器解决就不用锁能用原子类就不用锁把无锁方案的优先级提到加锁之前。但真到了需要 ReentrantLock 的那些场景——可中断、可超时、多条件队列——你会发现它提供的灵活性确实无可替代。把这些细节吃透不仅是面试能多拿分更重要的是线上出问题的时候你能比别人更快地定位到根因。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑