ReentrantLock独占锁在AQS框架下的实现与运作
Java并发包中的ReentrantLock是一个可重入的独占锁,它依赖于抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)框架来实现其核心功能。本文将深入探讨ReentrantLock(以非公平模式为例)的加锁与解锁机制,揭示其底层AQS的工作原理。
一、加锁(Lock)过程解析
当一个线程调用ReentrantLock实例的lock()方法试图获取锁时,实际上会委托给其内部的同步器Sync来处理。默认情况下,ReentrantLock使用非公平同步器NonfairSync。
1.1 首次尝试加锁
在非公平锁模式下,线程会直接尝试通过CAS操作抢占锁,而不会考虑AQS等待队列中是否存在其他线程。
final void lock() {
// 立即尝试通过CAS将AQS的状态从0设置为1
if (compareAndSetState(0, 1)) {
// 如果成功,说明当前线程抢到了锁,并将其设为独占线程
setExclusiveOwnerThread(Thread.currentThread());
} else {
// 如果首次尝试失败(锁已被占用),则进入AQS的通用获取流程
acquire(1);
}
}
如果compareAndSetState(0, 1)成功,表示当前没有任何线程持有锁,并且当前线程成功获取了锁,然后将当前线程设置为独占所有者线程,加锁过程结束。如果失败,说明锁已被其他线程持有,或者存在竞争,当前线程将进入acquire(1)方法,启动AQS的排队等待机制。
1.2 AQS通用获取流程:acquire(int units)
acquire(int units)是AQS框架中用于独占模式获取资源的模板方法。它包含了非公平尝试获取、构建等待节点、入队以及循环等待的逻辑。
public final void acquire(int units) {
// 尝试获取锁,如果失败,则构建等待节点并入队,然后在队列中等待
// 如果在等待过程中被中断,则设置中断标志
if (!tryAcquire(units) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), units))
Thread.currentThread().interrupt(); // 自我中断
}
这里有两个关键步骤:tryAcquire(units) 和 acquireQueued(addWaiter(Node.EXCLUSIVE), units)。
1.2.1 非公平模式下的tryAcquire(int units)
对于NonfairSync,其tryAcquire方法会调用nonfairTryAcquire。这是非公平锁的关键所在,它允许"插队"。
protected final boolean tryAcquire(int units) {
return nonfairTryAcquire(units);
}
final boolean nonfairTryAcquire(int units) {
final Thread current = Thread.currentThread();
int currentLockState = getState(); // 获取当前AQS状态值(重入次数)
if (currentLockState == 0) { // 锁当前处于空闲状态
// 再次尝试CAS获取锁,这次是"插队"尝试,不检查队列
if (compareAndSetState(0, units)) {
setExclusiveOwnerThread(current); // 设置当前线程为锁的持有者
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 当前线程已持有锁,进行重入
int newLockState = currentLockState + units;
if (newLockState < 0) { // 检查整数溢出
throw new Error("Lock count exceeded maximum");
}
setState(newLockState); // 更新AQS状态,增加重入计数
return true;
}
// 其他线程持有锁,或CAS失败
return false;
}
如果nonfairTryAcquire成功获取锁(无论是首次CAS还是重入),则acquire方法结束。如果失败,则需要将当前线程封装成等待节点并加入AQS的等待队列。
1.2.2 构建等待节点并入队
addWaiter(Node.EXCLUSIVE)负责将当前线程封装成一个独占模式的Node节点,并尝试将其加入到AQS等待队列的尾部。
private Node addWaiter(Node.LockMode mode) { // LockMode 代替原来的 mode
Node newNode = new Node(Thread.currentThread(), mode);
Node currentTail = tail;
// 快速路径:如果队列尾部不为空,尝试直接连接
if (currentTail != null) {
newNode.prev = currentTail;
if (compareAndSetTail(currentTail, newNode)) {
currentTail.next = newNode;
return newNode;
}
}
// 如果队列为空或快速路径失败,则进入enq循环确保入队
return enqueue(newNode);
}
这里的enqueue(Node node)方法是一个自旋CAS操作,用于在等待队列为空时初始化队列(设置头节点和尾节点),或在并发入队时确保节点能被正确地添加到队列尾部。
private Node enqueue(final Node newNode) {
for (;;) { // 自旋循环
Node currentTail = tail;
if (currentTail == null) { // AQS队列尚未初始化(头尾节点都为空)
// 尝试通过CAS初始化头节点,这是一个哨兵节点
if (compareAndSetHead(new Node())) {
tail = head; // 将尾节点指向头节点
}
} else { // 队列已初始化,尝试将新节点连接到尾部
newNode.prev = currentTail;
if (compareAndSetTail(currentTail, newNode)) {
currentTail.next = newNode;
return currentTail; // 返回前一个尾节点
}
}
}
}
1.2.3 循环等待与唤醒
acquireQueued(final Node node, int units)方法负责已入队的线程在队列中等待,并在满足条件时尝试获取锁或被唤醒。
final boolean acquireQueued(final Node node, int units) {
boolean acquireFailed = true;
try {
boolean wasInterrupted = false;
for (;;) { // 线程在此自旋等待,直到获取锁或被中断
final Node predecessor = node.getPredecessor(); // 获取当前节点的前驱节点
// 如果前驱节点是头节点(说明当前节点是队列中第一个真正等待的线程),
// 则再次尝试获取锁(非公平的又一次尝试)
if (predecessor == head && nonfairTryAcquire(units)) {
setHead(node); // 成功获取锁,将当前节点设为新的头节点
predecessor.next = null; // 解除原头节点的引用,利于GC
acquireFailed = false;
return wasInterrupted;
}
// 检查是否需要挂起当前线程
// 如果前驱节点状态表明它可以安全地发出信号,且当前线程可以安全地挂起
if (shouldParkCurrentThread(predecessor, node) &&
parkAndCheckForInterrupt()) {
wasInterrupted = true; // 如果在挂起期间被中断,设置中断标志
}
}
} finally {
// 如果获取锁失败(例如异常),则取消节点
if (acquireFailed) {
cancelAcquisition(node);
}
}
}
shouldParkCurrentThread(Node predecessor, Node currentNode):此方法决定当前线程是否可以安全地挂起。它主要处理前驱节点的waitStatus。
private static boolean shouldParkCurrentThread(Node predecessor, Node currentNode) {
int predStatus = predecessor.waitStatus;
if (predStatus == Node.SIGNAL) {
// 前驱节点处于SIGNAL状态,表示它会在释放时唤醒当前节点,可以安全挂起
return true;
}
if (predStatus > 0) { // predStatus > 0 意味着前驱节点已取消 (CANCELLED)
// 跳过所有已取消的前驱节点
do {
currentNode.prev = predecessor = predecessor.prev;
} while (predecessor.waitStatus > 0);
predecessor.next = currentNode;
} else {
// predStatus 为 0 或 PROPAGATE。尝试将前驱节点状态设为SIGNAL
// 以便它在释放时唤醒当前节点。返回false意味着当前线程还需要再次循环检查
compareAndSetWaitStatus(predecessor, predStatus, Node.SIGNAL);
}
return false;
}
parkAndCheckForInterrupt():调用LockSupport.park(this)将当前线程挂起,直到被唤醒或中断。被唤醒后,返回线程是否被中断。
private final boolean parkAndCheckForInterrupt() {
LockSupport.park(this); // 挂起当前线程
return Thread.interrupted(); // 返回并清除中断状态
}
1.3 AQS等待节点(Node)状态
AQS内部使用Node类来表示等待队列中的线程。每个Node都有一个waitStatus字段,用于表示节点的状态。
static final class Node {
static final Node SHARED_MODE_MARKER = new Node(); // 共享模式标记
static final Node EXCLUSIVE_MODE_MARKER = null; // 独占模式标记 (通常用null表示)
static final int CANCELLED = 1; // 节点已被取消
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 节点正在等待条件变量
static final int PROPAGATE = -3; // 共享模式下,释放后会无条件传播唤醒
// ... 其他字段如 thread, prev, next ...
}
二、解锁(Unlock)过程解析
当锁的持有者线程调用ReentrantLock的unlock()方法时,会触发AQS的资源释放机制。
2.1 unlock() 方法
public void unlock() {
sync.release(1); // 委托给同步器释放资源
}
2.2 AQS通用释放流程:release(int units)
release(int units)是AQS框架中用于独占模式释放资源的模板方法。
public final boolean release(int units) {
// 尝试释放锁,如果释放成功(AQS状态变为0),则唤醒等待队列中的下一个线程
if (tryRelease(units)) {
Node currentHead = head;
// 如果头节点不为空,且其waitStatus不为0(通常为SIGNAL,表示有后继需要唤醒)
if (currentHead != null && currentHead.waitStatus != 0) {
unparkSuccessor(currentHead); // 唤醒后继节点
}
return true;
}
return false;
}
2.2.1 尝试释放锁:tryRelease(int units)
tryRelease方法负责减少锁的重入计数,并在计数归零时释放锁的所有权。
protected final boolean tryRelease(int units) {
// 计算释放后的新状态
int newLockState = getState() - units;
// 只有锁的持有者才能释放锁
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
}
boolean isLockFullyReleased = false;
if (newLockState == 0) { // 如果新状态为0,表示锁完全释放
isLockFullyReleased = true;
setExclusiveOwnerThread(null); // 清除独占线程引用
}
setState(newLockState); // 更新AQS状态
return isLockFullyReleased;
}
如果tryRelease返回true(即isLockFullyReleased为true),说明锁已完全释放(重入计数归零),此时需要唤醒等待队列中的下一个线程。
2.2.2 唤醒后续节点:unparkSuccessor(Node headNode)
此方法负责找到AQS等待队列中下一个需要被唤醒的有效节点,并使用LockSupport.unpark()将其唤醒。
private void unparkSuccessor(Node headNode) {
// 获取头节点的waitStatus
int headStatus = headNode.waitStatus;
if (headStatus < 0) { // 如果状态是负数(如SIGNAL),尝试CAS将其设为0
compareAndSetWaitStatus(headNode, headStatus, 0);
}
// 目标是唤醒头节点的直接后继节点
Node successorNode = headNode.next;
// 如果后继节点为空或者已被取消,则从队尾向前遍历查找一个有效的、未取消的节点
if (successorNode == null || successorNode.waitStatus > 0) {
successorNode = null; // 重置,准备查找
for (Node current = tail; current != null && current != headNode; current = current.prev) {
// 找到第一个waitStatus <= 0 的节点(即未取消的节点)
if (current.waitStatus <= 0) {
successorNode = current;
}
}
}
// 如果找到了有效的后继节点,则唤醒它
if (successorNode != null) {
LockSupport.unpark(successorNode.thread);
}
}
被唤醒的线程会从其挂起的地方(parkAndCheckForInterrupt()之后)继续执行acquireQueued中的循环。它会再次检查自己是否是队列中的第一个等待线程,并尝试通过nonfairTryAcquire获取锁。如果成功,它将成为新的头节点,并继续其执行。如果失败,它将再次进入挂起状态,等待下一次被唤醒。