并发之AQS的双向链表
谈谈 AQS
AQS(AbstractQueuedSynchronizer)
是JUC
包下的一个抽象类。虽然是抽象类,但没有抽象方法,即便子类集成,也无法直接使用锁功能。AQS中关于锁的判断TryAcquire
与TryRelease
方法,默认都是报错,需要子类集成后进行重写,才能使用锁功能。
JUC
包的一些并发功能都是基于AQS
实现的,例如:ReentrantLock
、ThreadPoolExecutor
、并发集合等。
AQS的实现包含:一属性,两队列。
一属性:state
。表示锁的状态。初始化为0
表示无锁,获取锁后+1
,释放锁时-1
。一般在TryAcquire
与TryRelease
中实现+、-
功能。
两队列:双向队列与单向队列。
双向队列
双向链表,也称作等待队列、阻塞队列。是在线程获取锁失败后的等待处理操作。AQS中存在<Node> head
、<Node> tail
属性用于组成双向队列。
重点关注的几个重点词:
-
AbstractQueuedSynchronizer存在
head
与tail
属性,所以其本身就是一个链表。并没有使用集合 -
双向链表(等待队列)
head
永远都是伪节点(thead = null)tail
初始化时是伪节点(初始化时, head == tail),之后就不是了。
-
node的作用就是封装线程信息,然后并放到链表中排队
-
node节点有5种状态:
- 用于双向链表(CANCELLED、SIGNAL、0)
- 用于单向链表(CONDITION、CANCELLED)
- 用于共享锁(PROPAGATE)
-
双向链表中节点状态
tail
的节点状态永远是 0head
初始化为0,之后变为-1- 中间节点的状态为 -1
- 被取消(无效/中断)的节点状态为 1。
-
挂起线程:
LockSupport.pack(thread)
-
唤醒线程:
LockSupport.unpack(thread)
-
获取锁操作:
acquire -> tryAcquire -> addWaiter -> acquireQueued(死循环)
-> shouldParkAfterFailedAcquire -> parkAndCheckInterrupt -> LockSupport.park
-> setHead(可以认为,删除唤醒节点)
-
获取锁异常操作:
cancelAcquire
-
取消锁操作:
release -> tryRelease -> unparkSuccessor -> LockSupport.unpark
-
双向链表中是否可以不用head节点?
可以不用。- 在设计之前就提出了伪节点的存在,
- head节点的使用可以简化
-
为什么是双向链表,而不是用单线链表?
因为在使用单线链表时,删除中间节点时,无法将node.prev.next 指向node.next。解决方法只能不断遍历,增加了很多无用操作。
而使用双向链表就没有这个问题
锁的获取与释放 - 获取锁代码步骤
1. acquire 开启锁入口
所有调用锁功能的入口
public final void acquire(int arg) {
// 尝试获取锁
if (!tryAcquire(arg) &&
// 没有拿到锁,走此方法
// addWaiter(Node.EXCLUSIVE) 封装层一个node节点, 并放入到双向链表中
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 当前线程中断
selfInterrupt();
}
2. tryAcquire 尝试获取锁
尝试获取锁。需要子类方法去重写此类。返回true
,表示获取到锁,返回false
。表示未获取到锁,代码才会往后走。
大体逻辑:
- CAS,如果
AQS.state = 0
,则将AQS.state = 1
- 修改
aqs.thread = 当前线程
AQS
中对于TryAcquire
需要子类重新,这里,我们使用ThreadPoolExecutor.Worker
中TryAcquire
方法(主要是代码简单)
protected boolean tryAcquire(int unused) {
// cas 方式获取锁,就是尝试修改 state 参数
if (compareAndSetState(0, 1)) {
// 获取到锁(独占锁),将方法头 markwork 中线程改为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
3. addWaiter 将线程放入等待队列中
没有获取到锁,创建一个node节点,并放入双向队列的中,队列遵循FIFO。enq()是head与tail初始化方法。可以细看一下,代码简单,在这里不做描述。
private Node addWaiter(Node mode) {
// 创建一个节点,设置其同步队列中的下一个节点。
// 此处可以看出,node其实就是线程的封装
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果tail != null,说明队列中有值,将node放入到队列的末尾
if (pred != null) {
node.prev = pred;
// cas的经典实用
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果 tail 为null时,说队列没有值,需要初始化 head 与 tail
// 以死循环的方式,一定将node放入到队列的末尾
enq(node);
return node;
}
4. acquireQueued(死循环) 将刚加入线程,挂起
方法体中存在死循环,线程被挂起后,基本上两圈半后,就会返回 true,结束循环。
- 第一圈,将前置节点状态改为 -1。
- 第二圈,将线程挂起。
- 半圈,线程被唤醒后成为head,变为伪节点。
而被唤醒的线程一定是Head.next节点。当然如果,线程本身就是head.next,则直接过去到锁。不会挂起。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取node的前置节点
final Node p = node.predecessor();
// 如果 node.pre节点,也就是 p == head,说明node就是第一顺位节点。
// 这样,就尝试获取锁资源
// 如果不是,就放到队列里面
if (p == head && tryAcquire(arg)) { // tryAcquire方法,会被不同的子类重写,可以查看 ReentrantLock、ThreadPoolExecutor.Worker
// 获取锁资源成功,node就是变成 head,也变成 伪节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire(p, node)
// 功能:将 p节点状态改为 -1,并返回false。当返回true后,才会执行 parkAndCheckInterrupt方法,挂起
// 所以这个死循环,一般会走两边,
// 第一次循环,将p节点状态改为 -1
if (shouldParkAfterFailedAcquire(p, node) &&
// 第二次循环,将 node 节点挂起。LockSupport.pack(this)
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 在 acquireQueued 基本上不会走这个方法
if (failed)
cancelAcquire(node);
}
}
5. shouldParkAfterFailedAcquire 挂起前准备,将前置节点状态改为 -1
将 node.prev节点状态改为 -1,并返回false。当返回true后,才会执行 parkAndCheckInterrupt方法,挂起。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// ws == Node.SIGNAL 表示 pred节点可以唤醒后面的节点
// 只有 ws = -1,node才会挂起,并返回 true
if (ws == Node.SIGNAL) return true;
// ws > 0 ,只会是 1,表示此节点已被取消。
if (ws > 0) {
// 当前node节点的 pre节点的pre节点,作为 node的pre节点
// 知道找到 pred.waitStatus != 1的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* cas 将 pred 节点的状态改为 -1。表示可以唤醒后续线程
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
6. parkAndCheckInterrupt 挂起线程(LockSupport.park)
挂起线程,查看线程的唤醒,是因为异常报错,还是正常唤醒。
private final boolean parkAndCheckInterrupt() {
// 挂起线程方法
LockSupport.park(this);
// 这个方法可以确认,当前线程是中断唤醒的,还是正常唤醒的
return Thread.interrupted();
}
7. setHead 被唤醒的节点,变为head(可以认为,删除唤醒节点)
能走到setHead方法,说明已经获取锁。并将node代替head节点。
需要注意的事:head.next 不能设置为空。释放锁时用。
锁的获取与释放 - 释放锁代码步骤
1. release 释放锁入口
释放锁的两个动作:1.将AQS.state--,2.唤醒 head.next线程
public final boolean release(int arg) {
// tryRelease 如果锁释放,则返回true,否则false
if (tryRelease(arg)) {
// 锁已经放掉,走此逻辑
Node h = head;
// h != null,说明有排队的
// h.waitStatus != 0,说明有排队的,并且线程已经挂起
if (h != null && h.waitStatus != 0)
// 唤醒下一个线程等待节点
unparkSuccessor(h);
return true;
}
return false;
}
2. tryRelease 尝试释放锁
与TryAcquire
相同,需要子类重写此方法。
大体逻辑:
- 判断 AQS.state == 0。只有等于0,才说明完全释放锁,才会有后面动作。大于0,说明锁未完全释放。
- 设置 AQS.thread =null
为了能体现,AQS.state > 0
的情况,我们查看ReentrantLock
的TryRelease
方法
protected final boolean tryRelease(int var1) {
// 获取 AQS.state值。var1必然是1
int var2 = this.getState() - var1;
// 如果当前线程不是 拥有锁的线程,则报错
if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
boolean var3 = false;
// 判断 AQS.state == 0
if (var2 == 0) {
var3 = true;
// AQS.thread =null
this.setExclusiveOwnerThread((Thread)null);
}
// 如果是重入锁,此时调整锁的次数
// 实际上就是 state--
this.setState(var2);
return var3;
}
}
3. unparkSuccessor 唤醒下一个节点 LockSupport.unpark(head.next.thread);
唤醒 head.next
线程。被唤醒的节点,会继续执行acquireQueued
的方法。
假如head.next.waitStatus =1
,也就是说,下一个节点被取消了,不能唤醒。则从tail
上前寻找距离head
最近的有效节点,并唤醒。
通过
acquireQueued方法,可以知道被唤醒的节点最终会成为
head。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// ws<0,则 ws =1 或者 =-1
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 假如 s 节点被中断了。是null
// 将从 tail 向前找,找到距离head最近的有效节点,并赋值给s,进行唤醒
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;
}
// 如果不是 null,则将直接唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
锁的获取与释放 - 获取锁异常代码步骤
1. cancelAcquire 取消在AQS(双向链表)中的node
代码一般被写到finally块内,保证获取锁异常时一定执行
取消节点的操作流程:
- 将node.thread = null
- 往前找到有的节点作为node.prev
- 将 node.waitStatue = 1,表示节点取消
- 将node 脱离 双向链表,分三种情况
4.1 当node 是tail,直接删除
4.2 当node 是 head.next,删除 并唤醒下一个节点
4.3 当node 是中间节点,删除 并确保node.prev.waitStatus一定为-1
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
// 1. 将 thread 设置为 null
node.thread = null;
// 2. 往前找到 节点状态有效的节点,就是 !=1
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 3. node节点状态,改为已删除
node.waitStatus = Node.CANCELLED;
// 4.1 如果node节点是 tail,则将 prev 作为 tail节点
if (node == tail && compareAndSetTail(node, pred)) {
// 将 tail.next = null
// 感觉没有必要,直接 == null,就可以了
// compareAndSetTail可以保证,下一步操作的唯一
compareAndSetNext(pred, predNext, null);
} else {
// 代码走到这,说明node不是 tail
int ws;
// 4.3 node是中间节点,确保prev节点状态是-1,这样才可以指向next节点
if (pred != head &&
// 下面代码: 就是确保 prev的状态一定是 -1 ,这样就可以唤醒后续节点
((ws = pred.waitStatus) == Node.SIGNAL ||
// ws 并发情况下,有可能是0,但也要改为-1
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) { // 二次校验,prev是一个有效节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 下面代码的意思: prev.next = node.next
compareAndSetNext(pred, predNext, next);
} else {
// 4.2 如果 prev == head,说明node是head.next
// 则直接唤醒node.next节点
unparkSuccessor(node);
}
// 将自己指向自己,可以 gc 删除
node.next = node; // help GC
}
}