前言
在Java并发编程中,ReentrantLock是一个非常重要的可重入互斥锁,它比synchronized提供了更灵活的锁机制:支持公平/非公平模式、可响应中断、超时等待、多条件变量等。
但真正让ReentrantLock强大的,是其底层依赖的AbstractQueuedSynchronizer(AQS) 框架。AQS是JUC(java.util.concurrent)包的基石,CountDownLatch、Semaphore、ReentrantReadWriteLock等同步组件都基于它构建。
本文将逐行解析源码,彻底讲清楚:
-
AQS的核心数据结构(state、CLH队列、Node)
-
非公平锁的完整加锁/解锁流程
-
公平锁的实现原理及与非公平锁的区别
-
同步队列的入队、阻塞、唤醒机制
-
重入锁的实现原理
一、AQS核心架构
1.1 AQS是什么?
AbstractQueuedSynchronizer是一个抽象类,它提供了一个FIFO队列 来管理等待锁的线程,并定义了一个int类型的state作为同步状态。
子类只需要实现tryAcquire、tryRelease等钩子方法,即可构造出自己的同步器。
1.2 核心属性
java
// AbstractQueuedSynchronizer 中的核心属性
// 同步状态,volatile保证可见性
// 在ReentrantLock中:state=0表示锁空闲,state>0表示锁被持有(值表示重入次数)
private volatile int state;
// 同步队列的头指针(懒加载,初始化时为空)
private transient volatile Node head;
// 同步队列的尾指针
private transient volatile Node tail;
java
// AbstractOwnableSynchronizer(AQS的父类)中的属性
// 记录当前持有锁的线程(独占模式下使用)
private transient Thread exclusiveOwnerThread;
1.3 Node节点结构
同步队列中的每个节点都是一个Node对象:
java
static final class Node {
// 节点等待模式
static final Node SHARED = new Node(); // 共享模式
static final Node EXCLUSIVE = null; // 独占模式
// 等待状态常量
static final int CANCELLED = 1; // 线程已取消等待
static final int SIGNAL = -1; // 当前节点释放锁后需要唤醒后继节点
static final int CONDITION = -2; // 在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下需要传播唤醒
// 节点状态(重要:-1 SIGNAL,1 CANCELLED,0 初始状态)
volatile int waitStatus;
// 双向链表指针
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
// 该节点封装的线程
volatile Thread thread;
// 指向条件队列的下一个节点(Condition相关,本文暂不详述)
Node nextWaiter;
}
1.4 同步队列结构图

重要特性:
-
队列是FIFO(先进先出)的
-
头节点是一个哨兵节点,一般不关联具体的线程(或关联当前持有锁的线程)
-
每个节点封装一个等待的线程
-
节点状态
SIGNAL表示:前驱节点释放锁后会唤醒我
二、非公平锁完整源码解析
2.1 加锁入口:lock()
java
// ReentrantLock 构造方法
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 调用 lock() 时,实际执行的是 sync.lock()
public void lock() {
sync.lock();
}
NonfairSync.lock()实现:
java
final void lock() {
// 【第一次插队机会】快速CAS尝试获取锁
// 如果锁空闲(state=0),则直接抢锁成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 抢锁失败,进入完整获取流程
}
为什么叫非公平? 新来的线程可以不排队,直接尝试抢锁。如果抢成功了,队列中等待的线程只能继续等。
2.2 acquire:AQS模板方法
java
public final void acquire(int arg) {
// tryAcquire:尝试获取锁(包含重入逻辑)
// addWaiter:获取失败,将当前线程封装成节点加入队列
// acquireQueued:在队列中自旋等待
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 如果等待过程中被中断过,补上中断标记
}
2.3 tryAcquire:尝试获取锁
AQS中的钩子方法:
java
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
NonfairSync中的实现:
java
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
核心逻辑 nonfairTryAcquire:
java
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 【情况1】锁空闲
if (c == 0) {
// 【第二次插队机会】CAS获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 【情况2】锁已被当前线程持有 → 重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // 溢出保护
throw new Error("Maximum lock count exceeded");
setState(nextc); // 不需要CAS,因为只有当前线程才能执行到这里
return true;
}
// 【情况3】锁被其他线程持有
return false;
}
重入锁的关键 :同一个线程可以多次获取同一把锁,每次重入state加1,释放时需要释放相同次数。
2.4 addWaiter:入队操作
java
private Node addWaiter(Node mode) {
// 创建新节点,封装当前线程,mode=null表示独占模式
Node node = new Node(Thread.currentThread(), mode);
// 【快速尝试】如果队列已存在,尝试直接CAS追加到队尾
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 【兜底方案】队列不存在或CAS失败,进入自旋入队
enq(node);
return node;
}
2.5 enq:自旋入队(线程安全)
java
private Node enq(final Node node) {
for (;;) { // 自旋,直到入队成功
Node t = tail;
if (t == null) { // 队列尚未初始化
// 创建哨兵节点作为头节点
if (compareAndSetHead(new Node()))
tail = head; // 头尾指向同一个哨兵节点
} else {
// 标准入队操作
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t; // 返回前驱节点
}
}
}
}
为什么要创建哨兵节点? 有了哨兵节点,队列永远不会为空,简化了唤醒逻辑------每次从头节点的后继开始唤醒即可。
2.6 acquireQueued:排队等待
这是最核心的等待逻辑:
java
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取前驱节点
// 【关键判断1】前驱是头节点 → 说明当前节点是队列中等待最久的
if (p == head && tryAcquire(arg)) {
// 获取锁成功,将当前节点设置为新头节点
setHead(node); // 头节点会清空thread引用
p.next = null; // help GC
failed = false;
return interrupted; // 正常退出
}
// 【关键判断2】获取锁失败,判断是否需要阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; // 记录中断状态
}
} finally {
if (failed)
cancelAcquire(node); // 异常情况取消获取
}
}
核心理解:
-
只有前驱是头节点的节点,才有资格尝试获取锁(保证FIFO)
-
获取失败时,会阻塞自己,等待前驱节点唤醒
2.7 shouldParkAfterFailedAcquire:决定是否阻塞
java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 【情况1】前驱状态已经是SIGNAL
if (ws == Node.SIGNAL)
// 放心阻塞,前驱释放锁时会唤醒我
return true;
// 【情况2】前驱已取消等待
if (ws > 0) {
// 跳过所有取消的节点,向前找到第一个有效节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
}
// 【情况3】前驱状态为0或PROPAGATE
else {
// 将前驱状态设置为SIGNAL
// 注意:这里只是设置状态,返回false,外层会再尝试一次获取锁
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; // 当前不需要阻塞,外层会再循环一次
}
为什么设置SIGNAL后返回false? 因为设置完成后,前驱节点可能刚好释放了锁,所以应该再给当前节点一次获取锁的机会。
2.8 parkAndCheckInterrupt:阻塞线程
java
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 阻塞当前线程
return Thread.interrupted(); // 唤醒后返回中断状态并清除中断标记
}
LockSupport.park()会让线程进入WAITING状态,直到被unpark()唤醒。
2.9 加锁完整流程图
┌─────────────────────────────────────┐
│ lock() 调用 │
└─────────────────┬───────────────────┘
▼
┌─────────────────────────────────────┐
│ 快速CAS抢锁 (state=0→1)? │
└─────────────────┬───────────────────┘
成功 │ │ 失败
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ 获取锁成功 │ │ tryAcquire尝试获取锁 │
│ 设置owner线程 │ │ (含重入逻辑) │
└─────────────────┘ └───────────┬─────────────┘
▼
成功 │ │ 失败
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 获取锁成功 │ │ addWaiter │
└─────────────────┘ │ 入队 │
└────────┬────────┘
▼
┌─────────────────┐
│ acquireQueued │
│ 自旋等待 │
└────────┬────────┘
▼
┌─────────────────┐
│ 前驱是头节点? │
└────────┬────────┘
是 │ 否
▼
┌─────────────────┐
│ tryAcquire成功? │
└────────┬────────┘
是 │ 否
▼
┌─────────────────┐
│ 设置新头节点 │
│ 返回 │
└─────────────────┘
│ 否
▼
┌─────────────────┐
│ shouldPark... │
│ 确保前驱SIGNAL │
└────────┬────────┘
▼
┌─────────────────┐
│ parkAndCheck... │
│ 阻塞线程 │
└────────┬────────┘
│
(唤醒后重新自旋) ◄──────────────┘
三、解锁流程完整解析
3.1 unlock入口
java
public void unlock() {
sync.release(1);
}
3.2 release:AQS释放模板
java
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁
Node h = head;
// 头节点不为空且状态不为0(说明有需要唤醒的后继节点)
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒后继节点
return true;
}
return false;
}
3.3 tryRelease:释放锁(重入减1)
java
protected 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); // 更新state(不需要CAS,因为只有当前线程能执行这里)
return free;
}
关键点 :重入锁的释放是分层的。每次unlock()只减少一次计数,只有计数归零时,锁才真正释放。
3.4 unparkSuccessor:唤醒后继线程
java
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除SIGNAL状态
Node s = node.next;
// 如果后继节点为空或已取消,从尾部向前找第一个有效节点
if (s == null || s.waitStatus > 0) {
s = null;
// 为什么要从后往前?因为并发环境下next指针可能不完整,但prev是可靠的
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒线程
}
3.5 解锁流程图
┌─────────────────────────────────────┐
│ unlock() 调用 │
└─────────────────┬───────────────────┘
▼
┌─────────────────────────────────────┐
│ tryRelease: state减1 │
│ 判断是否完全释放 (c == 0)? │
└─────────────────┬───────────────────┘
否 │ │ 是
▼ ▼
┌─────────────────┐ ┌─────────────────────────┐
│ 返回false │ │ 清空owner线程 │
│ 锁仍被持有 │ │ 返回true │
└─────────────────┘ └───────────┬─────────────┘
▼
┌─────────────────────────────┐
│ head != null && │
│ head.waitStatus != 0? │
└───────────┬─────────────────┘
是 │ 否
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ unparkSuccessor │ │ 直接返回 │
│ 唤醒后继节点 │ └─────────────────┘
└─────────────────┘
四、公平锁实现原理
4.1 公平锁与非公平锁的核心区别
公平锁与非公平锁的唯一区别在于:尝试获取锁时,是否检查队列中已有等待线程。
| 特性 | 非公平锁 | 公平锁 |
|---|---|---|
| 新线程能否插队 | 能(两次插队机会) | 不能 |
| 队列中等待线程的优先级 | 无特殊保护 | 等待最久的线程优先 |
| 吞吐量 | 高 | 低 |
| 可能产生饥饿 | 是 | 否 |
| 默认选择 | 是 | 否 |
4.2 公平锁的tryAcquire实现
java
// FairSync 中的 tryAcquire 方法
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("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
4.3 hasQueuedPredecessors:检查队列中是否有等待线程
java
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 条件为真的情况:队列不为空,且头节点的后继节点不是当前线程
return h != t && // 队列中至少有2个节点(头节点 + 至少一个等待节点)
((s = h.next) == null || // 头节点的后继为空(极端情况)
s.thread != Thread.currentThread()); // 后继节点的线程不是当前线程
}
这个方法的核心逻辑:
-
如果队列为空 → 返回false(没有等待者)
-
如果队列只有哨兵节点 → 返回false(没有实际等待的线程)
-
如果队列中第一个等待线程是当前线程 → 返回false(重入场景,允许获取)
-
否则 → 返回true(有其他线程在队列中等待,当前线程不能插队)
4.4 公平锁加锁流程对比
非公平锁:
线程到来 → 尝试CAS抢锁 → 失败 → tryAcquire再尝试 → 失败 → 入队等待
公平锁:
线程到来 → tryAcquire检查队列 → 有等待者则直接入队 → 入队等待
(没有lock开头的快速CAS抢锁)
注意 :公平锁的lock()方法中没有开头的CAS抢锁:
java
// FairSync.lock() 直接调用 acquire,没有快速尝试
final void lock() {
acquire(1);
}
4.5 非公平的两次插队机会(回顾)
非公平锁之所以"非公平",在于新来的线程有两次插队机会:
-
第一次插队 :
lock()方法开头的compareAndSetState(0, 1) -
第二次插队 :
tryAcquire()中的compareAndSetState(0, acquires)
java
// NonfairSync.lock()
final void lock() {
if (compareAndSetState(0, 1)) // ← 插队机会1
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// nonfairTryAcquire()
if (c == 0) {
if (compareAndSetState(0, acquires)) // ← 插队机会2
// ...
}
五、完整加锁流程对比总结
5.1 非公平锁完整流程
线程调用 lock()
│
▼
【插队1】CAS(state=0→1) 成功? ──是──▶ 设置owner → 返回
│ 否
▼
tryAcquire() → nonfairTryAcquire()
│
├── state=0? ──是──▶ 【插队2】CAS抢锁
│ │
│ ├── 成功 → 设置owner → 返回
│ └── 失败 → 继续
│
├── owner=当前线程? ──是──▶ state+1 → 返回成功
│
└── 其他情况 → 返回失败
│
▼
addWaiter() → 创建节点加入队列
│
▼
acquireQueued() → 自旋等待
│
├── 前驱是头节点且tryAcquire成功? ──是──▶ 设为新头节点 → 返回
│
└── 否则 → shouldPark... → parkAndCheck... → 阻塞
5.2 公平锁完整流程
线程调用 lock() → acquire(1)
│
▼
tryAcquire() (FairSync版本)
│
├── state=0 且 队列中无等待者? ──是──▶ CAS抢锁成功 → 设置owner → 返回
│
├── owner=当前线程? ──是──▶ state+1 → 返回成功
│
└── 其他情况 → 返回失败
│
▼
addWaiter() → 创建节点加入队列
│
▼
acquireQueued() → 自旋等待 (与非公平锁相同)
5.3 关键区别总结表
| 比较项 | 非公平锁 | 公平锁 |
|---|---|---|
| lock()开头快速CAS | ✅ 有 | ❌ 无 |
| tryAcquire中state=0时 | 直接CAS抢锁(不管队列) | 检查队列无等待者才CAS |
| 插队次数 | 2次 | 0次 |
| 吞吐量 | 高 | 低 |
| 饥饿风险 | 有 | 无 |
六、常见问题深度解答
Q1:为什么AQS队列的头节点是空节点(哨兵)?
答:这是一个设计优化,主要原因:
-
分离"当前持有锁的线程"和"队列管理"的职责
-
释放锁时,直接从头节点的后继节点开始唤醒,逻辑统一
-
避免空指针判断,简化代码
Q2:为什么unparkSuccessor要从尾部向前遍历?
答 :因为并发入队时,next指针可能不是最新的:
-
在
enq()方法中,先设置prev和CAStail,最后才设置next -
如果刚设置完
tail但还没设置next,从头向后遍历会丢失新节点 -
但
prev指针一旦设置就不会改变,所以从尾部向前遍历是可靠的
Q3:非公平锁为什么性能更好?
答:因为减少了线程挂起/唤醒的次数:
-
公平锁:线程释放锁后,必须唤醒队列中的下一个线程
-
非公平锁:新来的线程可能直接抢到锁,避免了唤醒开销
-
在高并发场景下,减少上下文切换能显著提升吞吐量
Q4:如何实现超时获取锁?
答 :AQS提供了tryAcquireNanos方法,核心逻辑是:
java
private boolean doAcquireNanos(int arg, long nanosTimeout) {
// ...
for (;;) {
// 尝试获取锁...
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false; // 超时返回
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout); // 限时等待
// ...
}
}
Q5:AQS如何支持共享模式(如Semaphore)?
答:共享模式与独占模式的区别:
-
独占模式:
state表示锁是否被占用(0/1或重入次数) -
共享模式:
state表示可用资源数量(如Semaphore的许可数) -
共享模式的节点释放后,会传播唤醒后面的共享节点(
PROPAGATE状态)
七、总结
7.1 核心知识点回顾
| 组件 | 作用 |
|---|---|
state |
同步状态,ReentrantLock中表示重入次数 |
exclusiveOwnerThread |
当前持有锁的线程 |
head/tail |
同步队列的头尾指针 |
Node |
队列节点,封装等待线程和前驱后继关系 |
waitStatus |
节点状态,SIGNAL表示需要唤醒后继 |
LockSupport |
线程阻塞/唤醒的工具类 |
7.2 关键设计思想
-
CAS + 自旋:无锁化实现线程安全的入队操作
-
模板方法模式:AQS定义骨架,子类实现钩子方法
-
CLH队列变种:双向链表便于取消和唤醒
-
哨兵节点:简化边界条件处理
-
可重入设计:state计数 + owner线程判断
7.3 一句话总结
AQS通过一个volatile的state变量表示同步状态,通过一个FIFO的双向CLH队列管理等待线程,利用CAS+自旋实现无锁入队,通过LockSupport实现线程的阻塞与唤醒。非公平锁允许两次插队提升性能,公平锁通过hasQueuedPredecessors检查保证先来后到。