作为 Android 开发,我们每天都在和 “线程安全” 打交道 —— 比如 UI 线程不能做耗时操作、子线程不能更新 UI,而锁就是解决线程竞争的核心工具。ReentrantLock作为 Java 并发包(JUC)的 “明星锁”,比synchronized更灵活(可中断、可超时、支持公平锁),但其设计思想和原理却常被 “CAS”“AQS” 等术语吓退。
今天我们用一个 “公司会议室抢用” 的趣味故事,结合代码和时序图,把ReentrantLock讲得明明白白。
一、故事背景:为什么需要 ReentrantLock?
假设公司有 1 间 “独占会议室”(共享资源),10 个员工(线程)要抢着用。如果没规矩,会出现:
-
两个员工同时冲进会议室(线程安全问题);
-
员工 A 刚出来,员工 C 直接插队,排队的员工 B 有意见(公平性问题);
-
员工 A 进去后,忘了出来,其他人永远用不了(死锁风险);
-
员工 A 进去后,想再拿份文件,却被当成 “新用户” 排队(不可重入问题)。
ReentrantLock就像一个智能会议室管理员,能解决以上所有问题。它的核心设计目标是: “独占、可重入、可公平 / 非公平、可安全释放” 。
二、ReentrantLock 的核心设计思想
在讲原理前,先提炼 4 个核心设计思想(对应管理员的 “工作准则”):
| 设计思想 | 对应 “会议室规则” | 技术本质 |
|---|---|---|
| 1. 委托式设计 | 管理员不自己做判断,委托给 “策略小组” 处理抢锁逻辑 | 核心逻辑委托给Sync(继承 AQS) |
| 2. 策略模式 | 支持 “按排队顺序叫号”(公平)或 “有空就进”(非公平) | FairSync和NonfairSync实现不同策略 |
| 3. 基于状态的锁管理 | 用 “会议室占用计数” 记录状态(0 = 空,N = 被占用 N 次) | state变量 + CAS 原子操作 |
| 4. 独占式等待队列 | 抢不到的员工按顺序排队,避免拥挤 | AQS 的双向链表等待队列 |
三、原理拆解:用故事 + 代码讲透每一步
ReentrantLock的核心是内部类Sync(继承自AbstractQueuedSynchronizer,简称 AQS),所有锁操作(抢锁、释放锁)都由Sync及其子类实现。我们分 “抢锁”“释放锁”“可重入” 三个场景讲。
场景 1:抢锁(员工申请用会议室)
1.1 非公平锁:“有空就进,不管排队”(默认策略)
故事场景:
员工 A(Thread1)来申请会议室,管理员看会议室空着(state=0),直接让他进;
员工 B(Thread2)来申请,发现被占,只好排队;
此时员工 A 出来了(state=0),员工 C(Thread3)刚到,没排队直接冲进会议室(插队)—— 这就是非公平。
对应代码(简化核心逻辑) :
ReentrantLock的lock()方法会委托给NonfairSync的lock():
// ReentrantLock的lock()方法
public void lock() {
sync.lock(); // 委托给Sync子类
}
// 非公平锁的Sync实现(NonfairSync)
static final class NonfairSync extends Sync {
final void lock() {
// 第一步:尝试“插队”——CAS把state从0设为1(有空就进)
if (compareAndSetState(0, 1)) {
// 成功:记录当前持有锁的线程(员工A)
setExclusiveOwnerThread(Thread.currentThread());
} else {
// 失败:调用AQS的acquire(),进入排队流程
acquire(1);
}
}
// 核心:尝试抢锁(tryAcquire)
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 非公平抢锁的核心逻辑
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 当前员工(比如Thread3)
int c = getState(); // 获取会议室状态(0=空,1=被占)
if (c == 0) { // 会议室空着
// 不看排队,直接CAS抢锁(插队关键!)
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true; // 抢锁成功
}
}
// 下面是“可重入”逻辑(场景3讲)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 计数+1(比如员工A再进一次)
if (nextc < 0) throw new Error("锁重入次数溢出");
setState(nextc); // 更新状态
return true;
}
return false; // 抢锁失败,准备排队
}
关键细节:
compareAndSetState(0,1):CAS(Compare And Swap)是 “原子操作”,相当于管理员 “瞬间检查并标记会议室占用”,避免两个员工同时抢成功;- 非公平的核心是 “不判断队列”:即使有员工排队,新到的员工也能直接抢空会议室。
1.2 公平锁:“按排队顺序,先到先得”
故事场景:
员工 A 进会议室后,员工 B、C 依次排队;
员工 A 出来后,管理员必须叫排队的第一个(员工 B),员工 C 不能插队。
对应代码(公平锁的 tryAcquire) :
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 会议室空着
// 公平的关键:hasQueuedPredecessors()判断“是否有排队的前驱”
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑(同上)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) throw new Error("锁重入次数溢出");
setState(nextc);
return true;
}
return false;
}
}
// AQS的方法:判断“当前线程是否有前驱在排队”
public final boolean hasQueuedPredecessors() {
Node t = tail; // 队尾
Node h = head; // 队头
Node s;
// 逻辑:如果队列不为空,且当前线程不是队头的下一个,就是有前驱
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
公平锁 vs 非公平锁对比:
| 维度 | 公平锁(FairSync) | 非公平锁(NonfairSync) |
|---|---|---|
| 核心逻辑 | 抢锁前先看队列(hasQueuedPredecessors) | 直接 CAS 抢锁,不看队列 |
| 效率 | 低(频繁切换线程) | 高(减少线程唤醒开销) |
| 场景 | 需严格按顺序的场景(如秒杀排队) | 大多数场景(如普通线程安全逻辑) |
1.3 抢锁失败:进入 AQS 等待队列
如果抢锁失败(比如会议室被占),会进入 AQS 的 “排队流程”(acquire(1)):
-
addWaiter:把当前线程包装成
Node(排队号),加入 AQS 的双向链表尾部; -
acquireQueued:让线程在队列中 “阻塞等待”(
LockSupport.park()),直到被唤醒。
故事对应:
员工 B 抢不到锁,管理员给她发个排队号(Node),让她坐在走廊椅子上等待(park),直到前面的人出来。
场景 2:释放锁(员工用完会议室)
故事场景:
员工 A 用完会议室,告诉管理员 “我走了”,管理员检查:
-
如果员工 A 之前重入过(比如进了 2 次),就把 “占用计数” 减 1(state 从 2→1);
-
如果计数减到 0(完全离开),就叫下一个排队的员工(员工 B)。
对应代码(释放锁核心逻辑) :
// ReentrantLock的unlock()方法
public void unlock() {
sync.release(1); // 委托给Sync释放
}
// AQS的release()方法(Sync继承AQS)
public final boolean release(int arg) {
// 第一步:尝试释放锁(tryRelease由Sync实现)
if (tryRelease(arg)) {
Node h = head; // 队头(第一个排队的员工)
if (h != null && h.waitStatus != 0) {
unparkSuccessor(h); // 唤醒队头的下一个线程
}
return true;
}
return false;
}
// ReentrantLock的Sync实现tryRelease
protected final boolean tryRelease(int releases) {
// 计算释放后的状态(占用计数-1)
int c = getState() - releases;
// 校验:只有持有锁的线程能释放(防止别人乱开门)
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean free = false;
if (c == 0) { // 计数减到0,完全释放
free = true;
setExclusiveOwnerThread(null); // 清空持有者(会议室变空)
}
setState(c); // 更新状态(重入时只减计数,不释放)
return free;
}
// 唤醒下一个排队的线程
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) {
// 找有效的节点(跳过已取消的)
for (Node t = tail; t != null && t != node; t = t.prev) {
if (t.waitStatus <= 0) s = t;
}
}
if (s != null) {
LockSupport.unpark(s.thread); // 唤醒线程(叫下一个员工)
}
}
关键细节:
tryRelease必须返回true才会唤醒队列:只有当 “占用计数减到 0”(完全释放)时,才会叫下一个人;LockSupport.unpark:相当于管理员 “叫醒” 排队的员工,让他再次尝试抢锁(此时会议室已空)。
场景 3:可重入(员工多次进会议室)
故事场景:
员工 A 已经在会议室(state=1),突然想起文件没拿,又跟管理员说 “再进一次”。管理员认识他(判断当前线程是持有者),直接让他进,把 “占用计数” 改成 2(state=2);
后来员工 A 拿完文件出来,计数减为 1(没完全释放);再出来一次,计数减为 0(完全释放),会议室变空。
对应代码:
就是tryAcquire中这段逻辑(前面已贴,再重点标注):
else if (current == getExclusiveOwnerThread()) {
// 当前线程是持有者:计数+1(重入)
int nextc = c + acquires;
if (nextc < 0) throw new Error("锁重入次数溢出");
setState(nextc); // 更新状态(比如1→2)
return true;
}
为什么需要可重入?
比如一个递归方法加了锁:
private ReentrantLock lock = new ReentrantLock();
public void recursiveMethod() {
lock.lock();
try {
// 业务逻辑
if (condition) {
recursiveMethod(); // 递归调用,再次抢锁(重入)
}
} finally {
lock.unlock();
}
}
如果不可重入,递归时线程会自己等自己释放锁,导致死锁。
四、时序图:可视化整个调用流程
我们用公平锁场景画时序图,展示 “Thread1 抢锁成功→Thread2 抢锁失败入队→Thread1 释放→Thread2 被唤醒并抢锁成功” 的完整过程。
五、总结:ReentrantLock 的 “灵魂”
-
AQS 是基石:
ReentrantLock本身不实现复杂逻辑,而是委托给 AQS,利用 AQS 的 “状态管理” 和 “等待队列” 解决竞争; -
可重入是便利:通过 “校验当前线程是否为持有者 + 累加 state” 实现,避免递归死锁;
-
公平 / 非公平是选择:根据业务场景选策略 —— 公平保证顺序,非公平保证效率;
-
释放锁要严谨:必须在
finally中调用unlock(),防止线程异常导致锁无法释放(管理员不会忘关门)。
理解ReentrantLock后,你会发现 JUC 中很多工具(如CountDownLatch、Semaphore)都基于 AQS 设计,掌握这个 “模板”,就能一通百通!