JUC 基石之 AQS 原理
文章目录
一、简介,什么是 AQS
AQS 全称 AbstractQueuedSynchronizer(抽象队列同步器),是 Java JUC 包的核心组件,是一种提供原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架。ReentrantLock、ReentrantReadWriteLock、Semaphore、ThreadPoolExecutor 等底层都是基于 AQS 来实现的,可见它的重要性很高。
二、相关基础知识
1. synchronized 原理
synchronized 是 JDK 中的一个关键字,是一种锁,可以用在实例方法、静态方法上,也可用于代码块。具体使用方式参见:https://blog.csdn.net/hzj1998/article/details/95633410。
来看看下面这段代码:
public class SynchronizedTest {
public void test() {
synchronized (this) {
System.out.println("test");
}
}
public static void main(String[] args) {
SynchronizedTest synchronizedTest = new SynchronizedTest();
new Thread(() -> {
synchronizedTest.test();
}).start();
}
}
编译后使用 javap (javap -v -p)命令分解 .class 文件析汇编指令(如下截取 test() 方法部分),可发现是基于 monitorenter 和 monitorexit 指令来实现锁的。
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String test
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
浏览 Oracle 官方文档 JVM 规范中 monitorenter 与 monitorexit 指令说明:
monitorenter: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter。
The objectref must be of type
reference
.Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
翻译过来大概意思就是:
objectref 必须是引用类型。
每个对象都与一个监视器 monitor 相关联。当且仅当监视器有 owner 时,监视器才被锁定。当某个线程执行 monitorenter 指令试图获得监视器的所有权时,其过程如下所示:
当 monitor 关联的对象进入次数为 0 时,该线程拿到 monitor 所有权,进入 monitor次数 +1 。
当该线程已经拥有该 monitor 的所有权,则允许重入,进入 monitor 次数 +1 。
若 monitor 所有权已经被其他线程获取,则该进程阻塞,直到 monitor 进入次数为 0 再获取 monitor 所有权。
monitorexit : https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit。
The objectref must be of type
reference
.The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
翻译过来大概意思就是:
objectref 必须是引用类型。
执行 monitorexit 指令的线程一定是该 monitor 的所有者。(即必须先获取锁再释放锁)
执行 monitorexit 指令会将 monitor 的进入数 -1,当进入数变成 0 时,当前线程不再拥有 monitor 的所有权,此时其它被这个 monitor 阻塞的线程可以重新尝试这个 monitor 的获取拥有权。
查看 C++ ObjectMonitor 源码,构造器如下,主要变量 _recursions(线程进入次数),_owner(拥有该 monitor 的线程),_WaitSet(执行了Object.wait()方法的线程集合),_EntryList(阻塞的线程队列):
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
2. CLH 队列
CLH 队列(Craig, Landin, andHagersten)是一个 FIFO 单向队列,它将线程封装成节点组织成一个队列,保证先请求的线程先获取锁,队列中的每个节点只需要自旋访问队列中前一个节点的状态,当一个节点释放锁时,只有它的后一个节点才可以得到锁。
AQS 的核心数据结构 CLH 的变体,是双向队列,并且将自旋操作改为阻塞操作,当上一个节点释放锁之后通知下一个节点。
3. CAS 原理
CAS 全称 compare and swap,比较和交换,是乐观锁的一种。CAS 机制中需要三个参数:内存地址、旧的期望值、新值,当更新一个变量的时候,只有旧的期望值与内存地址当前实际值相同时,才会将内存地址对应的值修改为新值。如果多线程同时修改可能会出现 ABA 问题。
Java 中提供了 CAS 方法:sun.misc.Unsafe#compareAndSwapObject
,是 C 语言实现的,有兴趣的可以去了解一下底层源码,最终是通过 lock cmpxchg 指令实现的。
思考:CAS 是无锁实现吗?
三、AQS 解析
1. AQS 分层架构图
AQS 大致可分为五层,从上到下由深入浅,如果需要自定义同步器时,只需要实现 API 层某些接口就可以,不需要关注底层实现。例如 ReentrantLock 中 Sync 类实现了 tryAcquire、tryRelease 等方法,ReentrantLock 的 lock 方法就是调用的 AQS 的 acquire 方法,可以看出 ReentrantLock 只是 AQS 的一层 “皮”。
2. AQS 核心思想
AQS 实现原理其实和 synchronized 底层实现原理有异曲同工之妙,主要数据结构就是一个状态位 + 一个等待队列,大致流程就是使用 CAS 机制修改状态位,修改成功代表获取锁成功,修改失败进入等待队列等待。
简化版流程图:
在 AQS 中等待队列是 CLH 变体双向队列,前面说了使用双向队列是为了减少自旋带来的系统消耗,而改用阻塞形式,通过主动唤醒的方式来优化性能。主要数据结构如下,其中每个节点包含一个 waitStatus 状态位和 Thread 线程变量,当头结点释放锁资源后,会唤醒下一个有效节点进行锁获取。
在 AQS 中双向队列头结点是虚节点,即不存储相关线程信息,只有 next 指针有效,可以理解为 head.next 才是队列第一个元素。尾结点则是直接指向队列最后一个节点,tail 指向的就是队列最后一个节点。具体可以看 enq() 入队方法实现。
AQS 提供了共享模式和独占模式的锁,对应代码中就是以下两套:
// 独占模式
void acquire(int arg); // 独占模式获取资源
boolean release(int arg); // 独占模式释放资源
// 共享模式
void acquireShared(int arg); // 共享模式获取资源
boolean releaseShared(int arg); // 共享模式释放资源
一般来说一种同步器要么是独占模式要么是共享模式,即只需要实现独占或者共享对应的方法即可。在 ReentrantLock 中的同步器是使用的独占模式,所以只需要实现 tryAcquire() / tryRelease() 这一套即可。
当然也有两种都需要的,例如读写锁 ReentrantReadWriteLock,读是共享锁,写是独占锁。
3. AQS 数据结构
可以看到 AbstractQueuedSynchronizer 继承了 AbstractOwnableSynchronizer 类,而 AbstractOwnableSynchronizer 类只有一个成员变量 Thread exclusiveOwnerThread,代表成功修改 state 的线程,在 ReentrantLock 中理解为占有锁的线程。
综上,在 AbstractQueuedSynchronizer 类中主要有以下成员变量:
/**
* 占有锁资源的线程
*/
private transient Thread exclusiveOwnerThread;
/**
* 等待队列头结点,虚节点
*/
private transient volatile Node head;
/**
* 等待队列尾结点
*/
private transient volatile Node tail;
/**
* 同步状态,可以类比 synchronized monitor 中线程重入次数
*/
private volatile int state;
Node 主要成员变量:
static final class Node {
/**
* 表示线程以共享的模式等待锁
*/
static final Node SHARED = new Node();
/**
* 表示线程正在以独占的方式等待锁
*/
static final Node EXCLUSIVE = null;
/**
* 等待状态标志位,该标志位决定了该节点在当前情况下处于何种状态
*/
volatile int waitStatus;
/**
* 前驱节点
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 节点线程
*/
volatile Thread thread;
/**
* 1. 表示下一个在Condition条件队列中等待的节点(共享模式);
* 2. 表示是共享模式或者独占模式
*/
Node nextWaiter;
}
Node 的 waitStatus 取值枚举如下,其中大于 0 表示节点线程处于取消状态不再竞争锁,小于 0 表示该线程处于可以被唤醒的状态。
枚举值 | 含义 |
---|---|
0 | 初始化默认值 |
CANCELLED = 1 | 取消状态,表示线程已经取消获取锁 |
SIGNAL = -1 | 表示下一个等待节点线程在等待当前节点线程主动唤醒 |
CONDITION = -2 | 表示节点在条件队列中,节点线程等待唤醒 |
PROPAGATE = -3 | 释放共享资源时需要通知其他节点;当线程处在SHARED情况下,该值才会使用 |
4. AQS 源码分析
下面我们从 ReentrantLock 的实现来分析一下 AQS 源码。
(1) 获取锁
入口 ReentrantLock.lock() 方法。
// 继承自 AQS 的同步器,支持公平锁和非公平锁
private final Sync sync;
public void lock() {
sync.lock();
}
可以看到 ReentrantLock.lock() 实际上是调用的 Sync.lock() 方法,而 Sync 是继承自 AbstractQueuedSynchronizer,Sync 有 FairSync 和 NonfairSync 两个子类,分别代表公平锁和非公平锁,ReentrantLock 默认使用非公平锁,可以在构造器中指定使用什么锁。
在 ReentrantLock 中,公平锁是指锁是按照线程的先后顺序来获取的,先到先得;而在非公平锁中新来的线程会先尝试获取锁,失败了再进入等待队列。
// 默认使用非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 指定使用公平锁或者非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
a. 公平锁获取过程
1. FairSync.lock() & acquire() 公平锁开始获取锁
公平锁获取锁过程就是 acquire 操作,具体看 acquire 实现。
// FairSync.lock()
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2. FairSync.tryAcquire() 公平锁尝试获取锁
下面来看一下 FairSync.tryAcquire() 具体实现,先判断 state 是否等于 0(是否有线程获取到了锁),如果等于 0,再判断是否有其它线程在队列中等待获取锁,如果有则 tryAcquire 操作失败,如果没有其它等待线程,则 CAS 修改 state 获取锁,修改成功则将当前线程设置为锁 owner,如果 state 不等于 0 ,如果锁 owner 是当前线程,则可重入,直接 set state = state + 1,注意这里不 CAS 修改而是直接 set,是因为能执行到这里只有当前线程,没有竞争。
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
int c = getState();
// 等于0说明还没有线程获取到锁
if (c == 0) {
// 判断是否有其它线程在队列中等待获取锁,如果没有则CAS修改state尝试获取锁
// 这是保证公平锁的关键,非公平锁实现中没有判断hasQueuedPredecessors
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
// 将当前线程设置为锁owner
setExclusiveOwnerThread(current);
return true;
}
}
// state不等于0,说明有线程获取到了锁,判断是不是当前线程,如果是,则可重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 这里不CAS修改而是直接set,是因为能执行到这里只有当前线程,没有竞争
setState(nextc);
return true;
}
return false;
}
流程图如下:
3. addWaiter() 进入等待队列
回到 AbstractQueuedSynchronizer.acquire(int arg) 方法,如果 tryAcquire(arg) 获取锁成功了,此方法就执行完了,否则执行 addWaiter 方法将当前线程进入等待队列,acquireQueued 方法是对排队中的线程进行获锁操作。
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter() 方法是将当前线程封装成 Node 添加进等待队列,入队采用尾插法。主要流程:通过当前线程与锁模式创建一个 Node,pred 指向尾结点 tail,将新创建的 Node 节点的 prev 指针指向 pred,然后通过 compareAndSetTail CAS 操作设置尾结点。
addWaiter() 方法会先尝试一下将 Node 快速入队,如果失败了再进行自旋入队。
如果 pred 等于 null(说明等待队列为空),或者 pred 指针和 tail 指针指向的位置不同(说明被其它线程修改过),就进入 enq() 方法 CAS 自旋修改尾结点。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 尾节点不为空,说明队列已经初始化过
if (pred != null) {
node.prev = pred;
// CAS修改尾结点为新创建的节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 尾结点为空,或者CAS设置尾结点失败,都要执行enq方法重试入队
enq(node);
return node;
}
4. enq() 进入等待队列
enq() 方法,如果尾结点为空,说明队列中没有节点,需要初始化头结点和尾结点,如果尾结点不为空,那么步骤就和 addWaiter() 前半部分一样插入新节点,操作失败了一直自旋,直到成功插入新节点为止。
双端队列中,head 头节点为虚节点,并不存储任何信息,只是占位。真正的第一个有数据的节点,是在第二个节点开始的,即 head.next() 节点。而 tail 尾结点不是虚节点,tail 指向的是真正的队列中的节点。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾结点为空,说明队列中没有节点,需要初始化头结点和尾结点
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
等待队列中步骤:
5. acquireQueued() 对排队中的线程进行获锁操作
前面讲到线程进入等待队列,那么什么时候出队列继续获取锁呢,就在 acquireQueued() 方法中。
再回到 AbstractQueuedSynchronizer.acquire(int arg) 方法,addWaiter(Node.EXCLUSIVE) 执行完成后会执行 acquireQueued() 方法,acquireQueued() 方法是对排队中的线程进行获锁操作。
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued() 方法是对排队中的线程进行获锁操作。在这里获取锁是会自旋 + 阻塞的方式,然后判断当前节点的上一个节点是否头结点,如果是则表示当前节点是真实数据队列队首,可以进行锁资源的竞争,此时获取锁如果是非公平模式则可能会失败,在公平模式下一定会获取锁成功;如果上一个节点是头结点但没有获取锁成功,或者上一个节点不是头结点,则会判断是否需要阻塞当前节点线程,shouldParkAfterFailedAcquire() 方法是判断是否需要阻塞,parkAndCheckInterrupt() 方法是执行阻塞当前线程操作。
如果自旋期间出现异常,则会执行 cancelAcquire() 取消当前线程获取锁操作。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋,要么获取锁,要么中断
for (;;) {
// 获取上一个节点
final Node p = node.predecessor();
// 如果上一个节点是头结点,说明node是真实数据队列队首,可以进行锁资源的竞争
if (p == head && tryAcquire(arg)) {
// 获取锁成功,设置头结点为当前节点
setHead(node);
// head只有next有值,所以只需要设置next为空,等待GC回收
p.next = null; // help GC
failed = false;
return interrupted;
}
// 执行到这,有两种情况,1:p是头结点且当前获取锁失败,2:p不是头结点
// 这时需要判断当前线程是否需要阻塞,防止一直自旋
// 如果前驱非取消节点waitStatus为SIGNAL,则挂起当前线程
// 反过来理解,即如果当前节点waitStatus为SIGNAL,则表明下一个节点在等待当前节点主动唤醒它
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 由于for循环里面只有一个return出口,所以正常情况下failed一定等于false,
// 所以这里只有出现异常才会执行,出现异常需要取消当前线程获取锁操作
cancelAcquire(node);
}
}
6. shouldParkAfterFailedAcquire() 判断当前节点线程是否应该被阻塞
shouldParkAfterFailedAcquire() 方法,靠前驱节点判断当前节点线程是否应该被阻塞,返回 true 代表当前线程需要被阻塞。只有前驱节点 waitStatus 等于 SIGNAL 才会返回 true,如果前驱节点 waitStatus 大于 0(大于 0 表示节点被取消了) ,则往前找到第一个不大于 0 的节点,将该节点设置为当前节点的前驱节点。如果前驱节点不等于 SIGNAL(-1)也不大于 0,则 CAS 设置前驱结点的 waitStatus 为 SIGNAL,表示当前节点在等待前驱节点释放锁。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前驱结点waitStatus
int ws = pred.waitStatus;
// 如果前驱结点waitStatus=SIGNAL,表示当前节点在等待前驱节点释放锁,需要阻塞当前线程然后等待前驱节点唤醒
if (ws == Node.SIGNAL)
return true;
// 如果前驱结点waitStatus大于0说明被取消了,需要往前找到一个没有被取消的节点,
// 然后将找到的节点设置为当前节点的前驱节点
if (ws > 0) {
// 循环找到第一个不大于0的节点,即没有被取消的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
// 将找到的节点设置为当前节点的前驱节点
pred.next = node;
} else {
// 如果前驱节点waitStatus不等于SIGNAL也不大于0,设置前驱节点waitStatus=SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
7. parkAndCheckInterrupt() 挂起当前线程
parkAndCheckInterrupt() 方法,挂起当前线程,然后返回当前线程的中断状态。
注意,此时线程被阻塞到这里了,也就暂停自旋了,只有等待前驱阶段主动唤醒。这一块到后面释放锁模块继续跟进。
LockSupport.park()/unpark() 底层原理是“二元信号量”,执行 unpark() 方法会增加许可证,最多只有一个许可证,执行 park() 方法会减少许可证。
LockSupport.park() 是不可重入的。
LockSupport.park() 只负责阻塞当前线程,并不会释放锁资源。
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
// 注意,此时线程被阻塞到这里了,也就暂停自旋了,只有等待前驱阶段主动唤醒
LockSupport.park(this);
// 返回当前线程中断标记,并清楚中断标记
return Thread.interrupted();
}
8. cancelAcquire() 取消获取锁操作
cancelAcquire() 方法是用来取消节点获取锁操作,当一个节点变成 CANCELLED 后是不能再变成其它状态了。
一个节点在队列中有可能是 head 的 next,有可能是在队列中某一个节点,也有可能是尾结点。
- 如果当前节点是尾结点,则将前置未取消节点设置为尾节点。
- 如果当前节点既不是尾结点也不是 head 后继节点,则设置后继节点为前驱结点的后继节点。
- 如果当前节点是 head 后继节点,则唤醒后继节点。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 使当前节点不关联任何线程,变成虚节点
node.thread = null;
// 获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,则一直往前找到第一个waitStatus<=0的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 过滤后的前驱节点的后继节点
Node predNext = pred.next;
// 把当前节点的waitStatus设置为CANCELLED
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,则将前置节点设置为尾节点
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else { // 如果当前节点不是尾节点,或者将前置节点设置为尾节点操作失败,则进入此分支
int ws;
// 当前节点不是头节点后继节点,并且前驱结点waitStatus=SIGNAL、thread不为null,
// 就设置当前节点的后继节点作为当前节点的前驱结点的后继节点,即”删除“当前节点
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
// 当前节点有后继节点,并且后继节点没有取消
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 是头结点后继节点,或者以上条件不满足,则唤醒下一个节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
b. 非公平锁获取过程
1. NonfairSync.lock() & acquire() 非公平锁开始获取锁
非公平锁会先尝试 CAS 修改 state 获取锁,获取失败再进行 acquire 操作。和公平锁差异点就在 tryAcquire() 方法中,其它流程和上面讲到的公平锁流程一样,就不再重复讲解了。
// NonfairSync.lock()
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2. NonfairSync.tryAcquire() 非公平锁尝试获取锁
下面来看一下 NonfairSync.tryAcquire() 具体实现,可以看到 tryAcquire() 是直接调用的 Sync.nonfairTryAcquire() 方法。
比较 FairSync.tryAcquire() 和 Sync.nonfairTryAcquire() 两个方法可以看出,非公平锁比公平锁少了一个判断条件 hasQueuedPredecessors(),即当 state 等于 0 时候,公平锁还会判断等待队列是否有其它线程,而非公平锁不会判断,直接进行锁的竞争。
// NonfairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
// Sync.nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
之后的流程就和公平锁一样了,就不在赘述了。
(2) 释放锁
a. 公平锁 & 非公平锁释放锁过程
1. unlock() 释放锁
入口 ReentrantLock.unlock() 方法,直接调用 AbstractQueuedSynchronizer.release() 方法。
public void unlock() {
sync.release(1);
}
2. release() 释放锁
由于释放锁不用区分公平锁还是非公平锁,所以没有像加锁那样不同流程。
AbstractQueuedSynchronizer.release(int arg) 方法,先尝试释放资源,如果 tryRelease() 返回 true 代表释放锁成功,如果等待队列中还有节点,则需要唤醒它们,告诉它们可以醒来抢锁了。
h == null 代表头结点还是 null,队列还未进行初始化,即队列没有元素。
h.waitStatus == 0 代表后继节点还没有被阻塞,还在运行,不需要唤醒。
h.waitStatus < 0 代表后继节点可能被阻塞了,需要被唤醒。
public final boolean release(int arg) {
// 释放资源
if (tryRelease(arg)) {
Node h = head;
// head不为null说明队列中有等待节点,并且头结点的waitStatus不是初始化节点,则需要唤醒一个挂起的线程
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3. tryRelease() 尝试释放锁
当前线程如果不是锁的 owner 就抛出异常,由于 ReentrantLock 是可重入的,此时 c 可能还是大于 0,如果 c 等于 0,说明该线程多次重入都释放锁完成了,此时可以完全释放锁资源让其它线程来获取,返回 true,否则返回 false 代表当前线程还没有释放锁。
protected final boolean tryRelease(int releases) {
// 由于ReentrantLock是可重入的,此时c可能还是大于0
int c = getState() - releases;
// 当前线程如果不是锁的owner就抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 如果c等于0,说明该线程多次重入都释放锁完成了,此时可以完全释放锁资源让其它线程来获取
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
4. unparkSuccessor() 唤醒后继节点线程
unparkSuccessor() 主要就是唤醒正在阻塞中的后继节点,一般情况下,唤醒的就是当前节点的后继节点,如果后继节点处于取消状态,则找后面的有效节点。
注意,这里找有效的节点是从尾结点开始向前找的,至于为什么要从尾结点开始找而不从头结点开始找,这就要看 addWaiter() 的实现了,节点在入队过程中先把原来的尾结点设置为当前的前驱节点,再将尾结点指针指向自己,最后再设置原来的尾结点 next 指向自己,而这并不是原子操作,所以在并发情况下只有 prev 指针是可靠的。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 先CAS更新waitStatus为0,
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 一般情况下,唤醒的就是当前节点的后继节点
Node s = node.next;
// 如果后继节点不存在或者后继节点被取消了,就找后面的有效节点
if (s == null || s.waitStatus > 0) {
s = null;
// 找有效节点是从尾结点开始向前找
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果找到了还在等待锁的节点,则唤醒它
if (s != null)
LockSupport.unpark(s.thread);
}
5. 线程被唤醒
在获取锁的时候,线程会被挂起,执行到 LockSupport.park(this) 就暂停了,等待上一个节点主动唤醒,然后在这里释放锁,就继续往下执行了。
private final boolean parkAndCheckInterrupt() {
// 在这里挂起了
LockSupport.park(this);
// 被唤醒后,从这继续执行
return Thread.interrupted();
}
四、总结
本文只是从 ReentrantLock 的实现来讲解 AQS 的独占模式实现,还有共享模式以及 Condition 没有涉及到,这些由于内容都比较多,需要另起篇幅来展开分析。
本文分析了 AQS 的原理,可以看到其实和 synchronized 底层原理类似,都是使用一个资源位 + 等待队列来实现,通过了解这个原理,可以很快的应用到其它锁实现上,比如分布式锁如何实现。