Java并发编程:ReentrantLock与AQS原理剖析
前言
在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 核心属性
// AbstractQueuedSynchronizer 中的核心属性 // 同步状态,volatile保证可见性 // 在ReentrantLock中:state=0表示锁空闲,state>0表示锁被持有(值表示重入次数) private volatile int state; // 同步队列的头指针(懒加载,初始化时为空) private transient volatile Node head; // 同步队列的尾指针 private transient volatile Node tail;// AbstractOwnableSynchronizer(AQS的父类)中的属性 // 记录当前持有锁的线程(独占模式下使用) private transient Thread exclusiveOwnerThread;1.3 Node节点结构
同步队列中的每个节点都是一个Node对象:
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()
// ReentrantLock 构造方法 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } // 调用 lock() 时,实际执行的是 sync.lock() public void lock() { sync.lock(); }NonfairSync.lock()实现:
final void lock() { // 【第一次插队机会】快速CAS尝试获取锁 // 如果锁空闲(state=0),则直接抢锁成功 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 抢锁失败,进入完整获取流程 }为什么叫非公平?新来的线程可以不排队,直接尝试抢锁。如果抢成功了,队列中等待的线程只能继续等。
2.2 acquire:AQS模板方法
public final void acquire(int arg) { // tryAcquire:尝试获取锁(包含重入逻辑) // addWaiter:获取失败,将当前线程封装成节点加入队列 // acquireQueued:在队列中自旋等待 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); // 如果等待过程中被中断过,补上中断标记 }2.3 tryAcquire:尝试获取锁
AQS中的钩子方法:
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }NonfairSync中的实现:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }核心逻辑 nonfairTryAcquire:
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:入队操作
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:自旋入队(线程安全)
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:排队等待
这是最核心的等待逻辑:
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:决定是否阻塞
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:阻塞线程
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入口
public void unlock() { sync.release(1); }3.2 release:AQS释放模板
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)
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:唤醒后继线程
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实现
// 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:检查队列中是否有等待线程
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抢锁:
// FairSync.lock() 直接调用 acquire,没有快速尝试 final void lock() { acquire(1); }4.5 非公平的两次插队机会(回顾)
非公平锁之所以"非公平",在于新来的线程有两次插队机会:
第一次插队:
lock()方法开头的compareAndSetState(0, 1)第二次插队:
tryAcquire()中的compareAndSetState(0, acquires)
// 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检查保证先来后到。
