上一篇我们深入分析了Java线程中断机制,掌握了线程安全终止的核心逻辑。在多线程并发控制中,锁是保证临界区安全访问的关键。日常开发中最常用的两把锁是synchronized(JVM层面)和ReentrantLock(JUC层面),而ReentrantLock的底层实现完全依赖于AQS(AbstractQueuedSynchronizer)。
AQS是整个Java并发工具包(JUC)的基石:CountDownLatch、CyclicBarrier、Semaphore、ReentrantLock、ThreadPoolExecutor等核心组件的同步机制,本质上都是AQS不同工作模式的封装。AQS的独占模式,正是ReentrantLock(可重入独占锁)的核心实现逻辑。理解AQS独占模式,就等于掌握了JUC锁的底层原理,这也是后端面试中“并发编程”模块的必考知识点,高频问题包括公平锁与非公平锁、可中断锁、超时锁的具体实现。
本文将从AQS的核心设计出发,深入剖析独占模式的源码实现,再解析ReentrantLock如何封装AQS,最后给出实战选型建议,力求不仅让你明白“怎么用锁”,更透彻理解“锁是怎么实现的”。
一、开篇先理清:AQS是什么?核心设计思想
很多人觉得AQS复杂,本质是没有抓住它的核心——AQS不是一把具体的“锁”,而是一套通用的同步器框架。它定义了一个多线程访问共享资源的同步模板,其核心围绕“状态管理 + 队列等待 + 阻塞 / 唤醒”这三件事展开。
1. AQS核心组成(JDK8源码核心字段)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
// 1. 核心状态:volatile保证可见性,CAS保证原子修改(ReentrantLock的重入次数就存在这里)
private volatile int state;
// 2. 同步队列的头节点(虚节点,无实际线程)
private transient volatile Node head;
// 3. 同步队列的尾节点
private transient volatile Node tail;
// 4. 入队时使用CAS + 自旋保证节点插入原子性
private static final Unsafe unsafe = Unsafe.getUnsafe();
}
2. AQS核心设计思想(一句话讲透)
- 状态驱动
使用volatile int state表示同步状态。例如在ReentrantLock中,state=0表示锁空闲,state=N表示被同一个线程重入了N次。所有同步逻辑都围绕对state的CAS修改展开。
- 队列等待
未抢到锁的线程,会被封装为Node节点,加入一个双向CLH队列(使用虚拟头节点和尾插法)。这避免了线程忙等待(自旋)对CPU资源的无谓消耗。
- 阻塞 / 唤醒
通过LockSupport.park()/unpark()实现线程的阻塞与唤醒(底层依赖Unsafe和操作系统调用),并可以与中断机制配合,实现可中断锁。
- 模板方法
AQS定义了acquire、release等模板方法。子类(如ReentrantLock的内部类Sync)只需要重写tryAcquire、tryRelease等核心方法,即可实现不同的同步逻辑(如独占或共享模式)。
个性化感悟
AQS的设计精髓在于运用了“模板方法模式 + 策略模式”:它将同步的通用逻辑(队列管理、阻塞唤醒、中断处理)封装为固定的模板方法,而把具体的“抢锁/释放锁”策略留给子类实现。这正是JUC众多组件“复用性强、扩展性高”的核心原因,与我们之前讨论过的CountDownLatch(基于AQS共享模式)和CyclicBarrier(基于ReentrantLock和Condition实现)的设计思路一脉相承。
二、AQS独占模式核心源码(JDK8精准拆解)
AQS独占模式的核心是“一个线程持有锁,其他线程排队等待”。其核心流程为:抢锁→排队→阻塞→唤醒→释放锁。下面我们将聚焦最核心的acquire()(抢锁)和release()(释放锁)方法,拆解其底层逻辑。
1. 核心入口:acquire(int arg)(独占式获取锁)
这是AQS独占模式下抢锁的顶层模板方法,ReentrantLock的lock()方法最终就会调用此方法。源码如下(JDK8):
public final void acquire(int arg) {
// 核心逻辑:1. 尝试抢锁 2. 抢不到则入队 3. 入队后阻塞
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 若抢锁过程中被中断,恢复中断标记(不吞中断)
selfInterrupt();
}
}
拆解4个核心步骤
步骤1:tryAcquire(int arg)(尝试抢锁,子类实现)
- 作用:由子类(如
ReentrantLock)重写此方法,实现具体的抢锁逻辑(公平或非公平)。
- 特性:非阻塞、无等待,仅通过CAS修改
state值,成功则抢锁成功,失败则立即返回false。
- 关键:在AQS中,
tryAcquire等方法默认直接抛出UnsupportedOperationException,必须由子类重写。
步骤2:addWaiter(Node.EXCLUSIVE)(抢不到锁,封装为Node入队)
- 作用:将当前线程封装为一个独占模式(EXCLUSIVE)的Node节点,并采用尾插法加入到CLH队列的尾部。
- 核心逻辑(JDK8精简版):
private Node addWaiter(Node mode) {
// 1. 创建当前线程的Node节点(独占模式)
Node node = new Node(Thread.currentThread(), mode);
// 2. 快速尝试CAS尾插(避免加锁,提升性能)
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 3. CAS失败则自旋入队(保证最终入队成功)
enq(node);
return node;
}
关键点:CLH队列是一个双向链表,虚拟头节点(head)不关联实际线程,第一个有效的等待节点是头节点的后继。入队过程全程使用CAS+自旋,保证了线程安全。
步骤3:acquireQueued(Node node, int arg)(入队后阻塞等待)
- 作用:已经入队的线程,会在一个自旋循环中不断尝试抢锁,若抢不到,则会被阻塞(
LockSupport.park())。
- 核心逻辑(JDK8核心片段):
final 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; // 原头节点出队,帮助GC
failed = false;
return interrupted; // 返回是否被中断过
}
// 抢锁失败,判断是否需要阻塞(检查前驱节点状态)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) {
interrupted = true; // 被中断唤醒,标记中断状态
}
}
} finally {
if (failed)
cancelAcquire(node); // 抢锁失败,取消节点(清理队列)
}
}
关键规则:
- 只有前驱节点是头节点的线程,才有资格尝试抢锁,这保证了队列的FIFO(先进先出)特性。
- 在真正阻塞前,会先检查前驱节点的状态,避免无效阻塞,然后才调用
LockSupport.park()。
- 被中断唤醒时,仅仅标记中断状态,不会立即退出自旋循环(标准的AQS独占模式
acquire不响应中断,除非使用可中断的acquireInterruptibly)。
步骤4:selfInterrupt()(恢复中断标记)
- 作用:如果
acquireQueued方法返回true(表示线程在等待过程中被中断过),则调用Thread.currentThread().interrupt()恢复线程的中断标记。
- 原因:
acquireQueued内部被中断唤醒时,会清除中断状态。此处恢复是为了“不吞没中断”,让上层业务逻辑能够感知到中断的发生。
2. 核心出口:release(int arg)(独占式释放锁)
ReentrantLock的unlock()方法最终调用此方法。源码如下(JDK8):
public final boolean release(int arg) {
// 1. 尝试释放锁(子类实现)
if (tryRelease(arg)) {
Node h = head;
// 2. 释放成功,唤醒队列首节点的后继线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
核心拆解:
步骤1:tryRelease(int arg)(尝试释放锁,子类实现)
- 作用:由子类重写此方法,修改
state状态。例如在ReentrantLock中,state--,当state减为0时表示锁被完全释放。
- 关键:释放锁必须保证完全释放(对于重入锁,需要释放对应的次数),否则返回
false。
步骤2:unparkSuccessor(Node h)(唤醒后继线程)
- 作用:唤醒头节点(
head)的第一个未被取消的后继节点所关联的线程(通过LockSupport.unpark())。
- 核心逻辑:被唤醒的线程会从之前
parkAndCheckInterrupt()的阻塞处恢复执行,并再次进入acquireQueued的自旋循环,重新尝试抢锁。
三、ReentrantLock源码解析:AQS独占模式的封装
ReentrantLock是AQS独占模式的“典型应用”。其核心是通过内部抽象类Sync(继承自AQS)重写tryAcquire和tryRelease方法,从而实现可重入、公平/非公平等特性。
1. ReentrantLock核心结构(JDK8精简版)
public class ReentrantLock implements Lock, java.io.Serializable {
// 核心:内部同步器,继承AQS
private final Sync sync;
// 抽象同步器,重写AQS核心方法
abstract static class Sync extends AbstractQueuedSynchronizer {
// 释放锁(独占模式)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
// 当前线程不是锁持有者,抛异常(保证锁的独占性)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // state=0,完全释放锁
free = true;
setExclusiveOwnerThread(null); // 清空锁持有者
}
setState(c); // 更新state(重入锁需多次释放)
return free;
}
// 判断当前线程是否持有锁
final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
// 非公平锁(默认):抢锁时不排队,直接CAS抢
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 公平锁:抢锁时先检查队列,有等待线程则排队
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 公平锁核心:先检查队列是否有等待线程,无则CAS抢锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入逻辑:当前线程已持有锁,state++
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
// 构造方法:默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 构造方法:指定公平/非公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 加锁(委托给sync)
public void lock() {
sync.acquire(1);
}
// 解锁(委托给sync)
public void unlock() {
sync.release(1);
}
}
2. 核心差异:公平锁 vs 非公平锁(JDK8精准对比)
| 维度 |
公平锁(FairSync) |
非公平锁(NonfairSync,默认) |
| 抢锁逻辑 |
先检查队列(hasQueuedPredecessors()),有等待线程则老实排队。 |
直接CAS抢锁,抢不到再排队。 |
| 性能 |
较低(涉及频繁的线程切换和队列检查)。 |
较高(减少了线程上下文切换,吞吐量更好)。 |
| 公平性 |
严格FIFO,不会产生线程饥饿。 |
可能产生饥饿(新来的线程可能一直插队成功)。 |
| 适用场景 |
对公平性要求极高的场景(如某些金融交易系统)。 |
追求极致性能的普通业务逻辑场景。 |
3. ReentrantLock核心特性(实战高频)
(1)可重入
- 实现逻辑:在
tryAcquire中,如果判断当前线程已经是锁的持有者,则直接将state值增加(state++)。释放时state--,直到state=0才算完全释放。
- 实战价值:避免同一线程在递归调用或嵌套方法中重复获取已持有的锁而导致死锁。
(2)可中断锁(lockInterruptibly())
- 实现逻辑:基于AQS的
acquireInterruptibly()方法。在抢锁过程中,如果线程被中断,则会抛出InterruptedException,并停止等待。
- 对比:普通的
lock()(内部调用acquire)不响应中断,只记录中断状态;而lockInterruptibly()会立即响应并抛出异常。
(3)超时锁(tryLock(long timeout, TimeUnit unit))
- 实现逻辑:基于AQS的
tryAcquireNanos()方法。尝试在指定时间内获取锁,超时则返回false,避免了线程无限期阻塞。
- 实战价值:防止线程因长时间抢不到锁而阻塞,提升了程序的健壮性和响应能力。
四、实战避坑与选型指引(落地可执行)
1. 高频坑点(JDK8源码层面避坑)
坑点1:ReentrantLock忘记解锁,导致死锁
- 原因:未在
finally代码块中调用unlock(),若业务逻辑抛出异常,将导致锁永远无法释放。
- 解决方案:必须遵循
try-lock-finally-unlock的标准模板。
ReentrantLock lock = new ReentrantLock();
try {
lock.lock();
// 业务逻辑
} finally {
lock.unlock(); // 无论是否异常,必须解锁
}
坑点2:公平锁性能损耗被忽视
- 原因:盲目追求“公平性”,在非必要场景下使用
FairSync,导致系统吞吐量显著下降。
- 解决方案:除非是金融、核心交易等对公平性有严格要求的场景,否则应优先使用默认的非公平锁。
坑点3:重入锁释放次数不匹配
- 原因:线程重入锁N次,但只释放了1次,导致
state != 0,锁未被完全释放,其他线程将永远无法获取该锁。
- 解决方案:加锁与解锁次数必须严格匹配,在递归或嵌套调用中需特别注意层级。
坑点4:混淆ReentrantLock和synchronized
| 特性 |
ReentrantLock |
synchronized |
| 底层实现 |
JUC(AQS),Java API层面。 |
JVM层级指令(monitorenter/monitorexit)。 |
| 可中断 |
支持(lockInterruptibly())。 |
不支持,线程会一直阻塞。 |
| 超时抢锁 |
支持(tryLock(timeout))。 |
不支持。 |
| 公平/非公平 |
支持通过构造函数配置。 |
非公平(无法配置)。 |
| 条件队列 |
支持绑定多个Condition对象。 |
只关联一个隐式的等待队列。 |
2. 实战选型指引(精准落地)
- 优先使用synchronized:在JDK 1.6对其进行了大量优化(偏向锁、轻量级锁、锁消除、锁粗化等)后,其性能与
ReentrantLock已相差无几。由于synchronized语法简洁,且无需手动释放锁(由JVM负责),代码更安全,应作为首选。
- 考虑使用ReentrantLock的场景:
- 需要可中断锁、超时锁或公平锁特性。
- 需要复杂的线程协作,例如多个等待条件(使用多个
Condition对象,经典案例如生产者-消费者模型)。
- 需要更灵活的锁控制,例如在批量处理完一批数据后才统一释放锁。
对AQS和ReentrantLock的深入理解,是构建健壮高并发应用的基础。如果你想深入更多并发编程的底层机制,或者探讨其他热门开源项目的源码实现,欢迎在技术社区持续交流。
五、核心总结(关键点回顾)
- AQS独占模式核心是“状态管理(state)+ 队列等待(CLH)+ 阻塞唤醒(LockSupport)”。
state表示同步状态,CLH队列管理等待线程,LockSupport实现线程阻塞与唤醒。
- ReentrantLock是AQS独占模式的经典封装,通过重写
tryAcquire/tryRelease实现可重入。公平锁与非公平锁的核心差异在于抢锁前是否检查等待队列(hasQueuedPredecessors())。
- 实战要点:使用
ReentrantLock必须在finally中解锁;非公平锁在大多数场景下性能更优,仅在强公平性要求的场景下使用公平锁。