写这篇拆解之前,先说个背景。我这两年在带团队做高并发交易系统时,发现很多同学对 synchronized 很熟,但一提到 ReentrantLock 就只说得出“比 synchronized 灵活”,再往下问 AQS、公平锁、Condition,就答不上来了。可恰恰是这类“差一口气”的地方,线上排查死锁、优化吞吐时最容易卡壳。这篇我打算从线程安全的本质讲起,把 ReentrantLock 的核心机制、源码思路、实战用法和踩坑经验一次说透,适合正在啃 Java 并发、准备面试、或者已经在生产环境跟锁打交道但想补全细节的开发者。
1. 从 synchronized 到 ReentrantLock:为什么需要一把“可重入”的锁
1.1 线程安全问题的本质:先看懂资源竞争
你说“线程安全”,到底在防什么?一句话:多个线程同时访问共享可变资源,导致结果不可预期。这个不可预期来自三个条件的叠加——多线程、共享数据、非原子操作。缺一个都不成立。
举个例子,网上流传最广的 i++ 自增问题。i++ 在字节码层面是三步:读取 i 的值、计算 i+1、把结果写回。两个线程同时执行,完全可能都读到旧值 100,都算成 101,再都写回,最终结果是 101 而不是 102。这就是经典的丢失更新。
要解决它,核心思路就一个:把“读-改-写”变成原子操作,让同一时刻只有一个线程能进入这段代码。这个“同一时刻只允许一个线程进入”的机制,就是锁。
Java 里实现锁的方式有很多,从最老的 synchronized,到 java.util.concurrent.locks 包下的 ReentrantLock、ReadWriteLock、StampedLock,再到底层基于 CAS 的 AtomicInteger。它们的底层支撑其实高度统一——都离不开 volatile 的内存可见性、CAS 的原子性,以及队列/阻塞机制来管理等待线程。理解了这个,你就知道锁不是玄学,而是“原子性 + 可见性 + 线程调度”的组合拳。
1.2 synchronized 的先天不足:不是不能用,是场景受限
JVM 对 synchronized 做过大量优化,锁升级从偏向锁到轻量级锁再到重量级锁,大多数场景下性能并不差。但它的短板也很明显,主要有四块:
第一,获取锁的过程不可中断。线程拿不到锁就死等,你在外面没法让它停下来。这在某些需要超时控制的场景下很致命,比如一个资源可能被占很久,另一个线程不想无限等下去。
第二,无法实现公平性。默认是非公平的,新来的线程可能插队,理论上存在“饥饿”的可能。虽然实际中概率不高,但基于 synchronized 做公平控制基本没门。
第三,超时控制能力为零。没有“等待 3 秒还获取不到就放弃”这种 API,只能靠 wait(long) 配合手写循环,绕来绕去还容易出错。
第四,只能通过 wait/notify 做线程协作,而且 wait/notify 用起来很容易踩坑——必须在持有锁的代码块里调用,否则抛 IllegalMonitorStateException,多个条件等待的场景更是要命。
这四点,正好是 ReentrantLock 要补的位。它和 synchronized 一样支持可重入(同一个线程可以重复获取同一把锁),但额外提供了中断响应、超时获取、公平锁、多 Condition 精确唤醒等能力。用不用它不是“比性能”这么简单,而是“synchronized 能不能表达你的并发控制需求”的问题。
1.3 ReentrantLock 核心特性一览:先建立全景图
ReentrantLock 从名字拆解就是“可重入的锁”。重入的意思是,同一线程已经拿到锁之后,再次进入同步代码块不需要重新排队抢锁,只需要对内部计数器加一即可。这个特性保证了一个场景的正确性:一个方法持有锁后又调用另一个需要同一把锁的方法——如果没有可重入性,这里就直接死锁了。
它最关键的几个能力,我列成速查,后面逐个展开:
- 基于
AbstractQueuedSynchronizer(AQS)实现,核心是一个volatile int state配合 FIFO 等待队列。 - 支持公平模式和非公平模式,构造器传
true开启公平。 lock()阻塞获取;tryLock()立即尝试;tryLock(timeout, unit)带超时;lockInterruptibly()可在等待时响应中断。newCondition()可以创建多个 Condition,实现比wait/notify精细得多的定向唤醒。- 必须手动
unlock(),且要与lock()成对出现,一般放在finally里。
我先把结论放这儿:ReentrantLock 不是 synchronized 的简单替代品,而是一套更精细的并发控制工具。它的复杂换来的是灵活,代价是更容易写错。下面我拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock 内部机制深度拆解:从源码层面看锁怎么工作
2.1 可重入性到底怎么实现的:一个计数器搞定
ReentrantLock 的可重入实现,比很多人想象中简单。它内部有一个整型变量 state,多少次获取锁成功,state 就加多少。
当线程 A 第一次拿到锁时,state 从 0 变成 1,AQS 的 exclusiveOwnerThread 字段记录下“当前持有锁的线程是 A”。A 再次调用 lock() 时,发现 exclusiveOwnerThread 就是自己,于是不排队、不竞争,直接把 state 变成 2。直到所有未配对的 unlock() 都执行完,state 归 0,锁才真正释放,其他线程才有机会获取。
这个设计非常优雅,它把“锁被谁持有”和“持有了几次”这两个信息用两个字段就表达清楚了。state 就是重入次数,exclusiveOwnerThread 就是所有者线程。
对应到源码层面,NonfairSync.tryAcquire 最终会调用到 nonfairTryAcquire,核心逻辑如下(我用伪代码还原思路):
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 锁空闲,CAS 尝试获取,成功后记录 owner
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
