万字讲解AQS队列 , 从入门到精通, 你值得拥有
快速导航
AQS (AbstractQueuedSynchronizer) 是 JUC 包下的一个核心… 它提供我们 实现自定义同步器的一个快捷模板
只要覆盖 一些方法 , 就能够自己写出一些同步器
* <li> {@link #tryAcquire}
* <li> {@link #tryRelease}
* <li> {@link #tryAcquireShared}
* <li> {@link #tryReleaseShared}
* <li> {@link #isHeldExclusively} //如果要使用等待队列..Condition , 那么首先 这个同步器 是要支持独占模式的, 还要重写这个方法
// 这些方法 .. 可以覆盖.
从 大方向上来讲, AQS 底层使用了 Node 作为基础的存储单元, 其实现了一个 阻塞队列 和 一个 条件队列
因此让 AQS 具有了 2种模式, 共享模式 和 独占模式
通常情况下, AQS 只使用其中一种模式 来搭建同步器, 当然也可以同时使用2种模式
独占模式的含义是 : 同时间 仅仅只有 一个线程在工作 , 其他的线程会阻塞在这个同步器上
共享模式的含义是 : 一旦释放锁 , 阻塞的所有线程 都会开始工作 .
底层的Node节点
首先来看底层的具体存储…
static final class Node {
// 该节点有2种存储模式
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// 记录该节点的状态
static final int CANCELLED = 1; // > 0 的情况, 是一种取消的了的状态 也就是说 只有在 <= 0 的情况下, 节点 才可能是正常工作的 (这个是表示自己的)
static final int SIGNAL = -1; // 表示需要信号 , 也就是需要被唤醒 (注意 : 在阻塞队列中, 节点上的状态, 通常表示的是下一个节点 需要的 情况)
static final int CONDITION = -2; // 表示需要等待一个条件
static final int PROPAGATE = -3; // 不常用... 表示可以传递而已
volatile int waitStatus; // 有上述的4个取值, 还有就是 0 , 表示没有状态
volatile Node prev; //阻塞队列的前一个节点
volatile Node next; //阻塞队列的后一个节点
volatile Thread thread; //当前线程
Node nextWaiter; //条件队列中的下一个节点
final boolean isShared() { //判断节点是不是共享的
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException { //获得前一个节点
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
小总结 :
- 简单的说 : 底层使用的node节点类似一个双向链表, 但是多出了一个 nextWaiter 节点, 该节点记录了是哪一个线程 , 这个节点可以抽象的看成 就是当前线程.
AQS的重要成员属性
看过了底层的Node节点, 我们来看一看 作为同步器 有哪些成员变量
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
十分简单… 就是保存了一个 头节点, 尾节点, 和一个状态… 这个状态 表示 ( 0 没有线程抢占, >0的情况 , 是同一个线程重入了多少次)
从独占模式开始学习
独占模式对应了2个方法
acquire
和acquireInterruptibly
: 字面上也能看出来, 就是一个方法能够响应中断, 一个不能响应而已, 来看看如何演绎
请求成功Or失败排队逻辑
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//这个方法是要求子类重写的
//因此 想要使用独占模式 (这个方法一定要重写)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//这是一个排队的逻辑处理
private Node addWaiter(Node mode) { //模式 就just是 独占模式 , 这个参数 一般不会发生变化
Node node = new Node(Thread.currentThread(), mode); //封装成底层的Node节点
//接下来是让node节点进行排队
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node; //先使用一次CAS , 如果排好了就正常返回
}
}
enq(node); //如果第一次CAS 失败了, 就会进入 enq方法 或者 是tail 节点为null ,意味着没人使用这个同步器
return node;
}
// 这个方法其实也很简单, 从它的名字来看, enq , 就是 进入队列 的意思
// 内部使用的 就是 一个自旋的CAS的操作
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;
}
}
}
}
这是一个基本的请求锁的逻辑 一直到排队的逻辑 :
- 首先是尝试获得锁 (tryAcquire) 如果获得成功 那就结束了
- 如果获取失败, 就会让线程进入一个addWaiter的方法 , 进行一个排队
- 这个方法首先会判断一下 tail 节点是不是为空
- 如果 tail 节点不为空 就进行一次 cas , 看是否排队成功
- 排队成功 就结束 排队逻辑, 如果排队不成功 或者 tail节点为空, 就会进入 enq方法
- 在enq 方法里, 它是一个 自旋的入队方法, 因此肯定是可以进入阻塞队列的 (这里要注意一下 enq的返回值是 当前节点的前一个节点 : 虽然在这个排队逻辑中 没有使用到 )
排队成功进入正式的阻塞环节
经过一个正常的排队逻辑之后, 该节点 (线程的抽象) 成功的进入了阻塞队列, 让我们接下来看 acquireQueued
方法 , 看看如何进行一个阻塞操作, 让进入阻塞队列的线程停下来
// 首先 我们要注意, 这个参数 node , 就是当前线程的抽象 node (arg 基本都是1)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //这个自旋 就是主要的逻辑
final Node p = node.predecessor(); // p是 当前node 的前一个node
//进入这个判断语句的条件 是 :
// 1. 首先这个节点的前一个节点是 头节点
// 2. 会再去尝试获得锁... (所以说这个方法很重要) (如果得到锁成功了 就进入了这个判断语句块中)
if (p == head && tryAcquire(arg)) { //这个自旋的唯一出口 , 就是进入这个循环 (当然还有就是发生了异常...)
setHead(node); // 将当前的节点 设置成为head节点.. 在这里我们也可以看出端倪. head节点应该表示的是正常运行的节点
p.next = null; // help GC
failed = false;
return interrupted; // 这里是做了一个 中断的判断...需要查看下面的方法
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally { //这个就相当于这个方法的安全网...
// 上面也说了.. 如果该自选的方法... 如果走正常出口, 那么failed 一定是 false ..
// 也就是如果要调用这个方法.. 代价就是 抛出了异常
if (failed)
cancelAcquire(node); // 会让这个节点取消请求的过程
}
}
这里先做出一个小补充, 关于 AQS 的 阻塞队列 :
- 其实AQS的阻塞队列 是不包括 头部的 , 也就是 head 节点 不包括在内
- 应该是从第二个节点开始, 才算是阻塞队列 (因为 head 节点在 独占模式的情形下, 是唯一正在run的 线程)
有了这个基础, 我们再回到 之前的方法中
- 对于特殊位置的节点, 才有可能进入到 判断块中成功获得锁
- 那么我们先来看 大多数的node 节点所要经过的方法
shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
// 这个命名真的绝了... 在请求失败之后应该被挂起 ...
// 先来看参数, pred 是当前节点的前一个节点, node 是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//这个方法 主要 就是在修改前一个节点的waitStatus ...
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 前一个 是signal ... 其实就是表示已经进入过这个方法了 (而且成功设置了)
return true;
if (ws > 0) { // 在之前的逻辑 是排队逻辑.. 这里是要开始阻塞线程的逻辑中
do { // 因此在做一个类似排队检查的过程中...发现 前面的节点 早就溜了... 那就可以把空缺补上
//进入这里 就意味着 前面的节点提前取消了.... 可能是中断了. 所以 要把前置节点请出去
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // 修改 前面的节点
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false; // 如果不是 前面一开始 不是 SIGNAL , 就返回 false , 让它自旋 再次进入判断
}
// 在 确保了前一个节点是 SIGNAL 的情况下 (进行挂起, 真正的停止)
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //使用的是park方法 到这一步, 线程就停止了. 独占模式请求 锁的逻辑就结束了
return Thread.interrupted(); // 这里是判断一下 从park状态出来 是不是因为中断了..
// 如果是因为中断, 也就是意味着 前面的方法 全部都是true , 就会标记 interrupted = true
// 这里值得注意的地方, 就是 就算是中断了, 也依然不会剔除 , 直到这个node 抢到了锁, 才会判断 , 会调用 selfInterrupt()
}
static void selfInterrupt() { //因为之前调用了 interrupted() , 所以恢复一下中断状态
Thread.currentThread().interrupt();
}
// LockSupport.park()
// 根据 源码的解释, 在线程进入park方法之后
// 只有3种情况能够从park状态中出来
/*
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
* <li>The call spuriously (that is, for no reason) returns.
* </ul>
1. 其他的线程对这个线程 调用了 unpark方法
2. 这个线程被中断了
3. 发生了不明的原因...
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
有一点大的小总结 :
- 到这里为止, 基本上
acquire
的逻辑全部的捋清楚了 - 在这个阶段里, 如果节点是在特殊的第二个位置, 那么会进行一次 tryAcquire 进行获得锁
- 得锁成功 那就出去了, 正常执行逻辑
- 如果失败了 , 就会进入一个 修改前节点 的逻辑
- 在挂起前的状态修改操作中
- 如果前节点已经被注明 是 SIGNAL 了, 那就返回true , ( 然后进入真正的阻塞逻辑 )
- 如果前节点已经被取消了 , 那就全都 “请出去” ,直到一个不是 取消状态的 前节点 作为节点
- 不然就 对前节点进行修改, 该成 SIGNAL 状态 (在后面2种情况下, 都会返回false 也就是会回到第一个步骤 进行自旋)
- 进入逻辑阻塞的阶段, 使用LockSuppose 进行 park 操作 , 等待 从park 状态恢复过来
- 出现下列3种情况的一种就会恢复过来 (1. unpark 2.中断 , 3.无原因 , (几乎不会出现))
- 恢复过来之后, 第一件事情 就是查看一下是不是 由于中断, 由于中断的话. 就标记一下 interrupted = true , 依然进行自旋等待
- 直到 该 node 抢到了锁
- 如果已经是中断的线程, 那么在node 抢到锁的之后, 会对该线程 进行一个中断恢复 , 如果没有中断, 那就开始开始正常的执行
取消节点的过程 —
等等 等等 等等, 在开始下一个环节前, 我们来思考一下, 这个仅仅只是
acquire
方法, 它是一个不响应中断的设计让我们简单的瞄一眼 响应中断的
acquireInterruptibly
方法
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果已经中断了, 那就 抛出中断异常
throw new InterruptedException();
if (!tryAcquire(arg)) //尝试获得锁, 失败之后 进入
doAcquireInterruptibly(arg);
}
// 这个方法几乎和 acquireQueued 一模一样,
// 唯一的区别 在于这方法 不返回 值了, 而且 不是标记中断状态, 而是直接抛出中断异常
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed) //那么进入这个方法的概率就比 acquiereQueued 高了
cancelAcquire(node);
}
}
// -------- 新东西! 注意 . 取消这个节点
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
while (pred.waitStatus > 0) //清除前面也已经取消了的节点
node.prev = pred = pred.prev;
Node predNext = pred.next;
node.waitStatus = Node.CANCELLED; //将自己标记为已经取消的节点
if (node == tail && compareAndSetTail(node, pred)) {
// 如果当前为tail , 就交换一下 (当然在判断的时候, 可能有其他的node会跟上, 跟上的话 , 就会把这个节点请出去了..)那就会进入下一个分支
compareAndSetNext(pred, predNext, null); //将 pred的下一个节点 修改成null . 全部清空
} else {
// 来到这个分支的情况分析 :
// 1. node != tail , 也就是可能是第二个位置的节点, 也可能是正常的节点 (不可能是head .. 因为head已经开始运行了)
// 2. 之前 node 还是tail , 在判断的极端的时间内, tail被别人抢去了 (这种情况下, 其实node节点以及被后来者请出队列了)
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 进入这个分支, 首先 node的位置 并不是第二个, 其次前一个节点并没有取消
Node next = node.next;
// 做的事情就是把自己的后面的节点, 都交给前面一个节点
// 让我们来思考一下 这个东西 真的真的正确吗
// 先判断第一个情况, 并没有被后节点请出去, 这就意味着 后节点 已经阻塞住了 , 所以很自然的成功的
// 第二种情况(极少发生) 已经被请出去了, 意味着 next 有可能为null , 但是不管有没有 这个cas 都不会成功了
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 进入这个分支, 也就是node 在那个特殊的位置, 那个可能得到锁的位置 , 这个时候, 就要唤醒一下后面的节点, 问问他能不能得到锁了
unparkSuccessor(node);
}
node.next = node; // help GC (把自己扔出去了)
}
}
// 比较简单的一个方法, 就是唤醒 后一个 没取消的节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); //如果当前节点没有取消, 就把状态回归到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); //进行唤醒
}
小小小小总结 :
- 不响应中断的node, 只有出现异常 才会进入 取消节点的环节, 响应中断的node , 中断之后 , 会进入 取消节点的环节
- 如果取消的节点处在一个特殊的位置, 就会唤醒后方的节点, 让整个流程变的可用, 安全
也就是说, 如果不响应中断的话, 节点 基本上不会变成 -2 cancel 的状态
释放锁的环节
在上文中, 我们已经成功的把一个线程park住了…
而且也说过, 只有头节点 , 才可能在运行, 也就是 可能去做一个释放锁的逻辑
在 独占模式下的, 释放锁的逻辑, 对应的方式 是 release
.
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
// 在注释中, 也已经表示了, 这是在独占模式下的释放锁的方式 , 可以用来实现 unlock
// 不过必须实现 tryRelease
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) // 思考一下 ? 当什么情况下 h.waitStatus 不为0 ?
// 答案很明了, 在上文中, 我们看清了阻塞的情况, 里面有一步就是修改前置节点的状态 , 让它变成 -1 SIGNAL 状态
// 所以 waitStatus 不为 0
unparkSuccessor(h); // 这个方法, 在上文也看过了, 就是唤醒 下一个节点 如果有的话
return true; //放锁成功
}
return false; //释放锁失败
}
// 如果要使用独占方法的话, 一定要实现 tryRelease方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
小总结 :
- 释放锁的逻辑十分的简单… 只要通过 tryRelease 的逻辑判断, 就能正常的释放锁 , 唤醒下一个节点
- 也就是说 tryRelease中 应该不光需要判断 有没有 release的资格, 而且还需要 正在的释放锁
- 否则如果出现了 这边唤醒了 下一个节点…下个节点通过自旋 又得到锁失败了, 又陷入park状态…这就死锁了- -…
到这里为止! 我们已经讲清楚了 抢锁 和 释放锁 的逻辑. 让我们来看看 JUC 包下 , 实际是如何做到的
来一个 ReentrantLock 的源码放松一下
reentranLock , 在锁升级(JDK1.6)前 , 是一个 首选使用的锁, 性能又好 , 又能支持 synchronized 做不到的特性
比如说 可以中断 啊 , 能够多个条件等待啊
对于这个锁来说 , 里面有2个内置的 同步器, 一个是 公平锁, 一个是非公平锁 (默认) , 他们都是基于 Sync 类的, 而这个类 是基于 AQS的, 我们来看看它是如何实现的
类的内容有点冗长 , 我们就来看 它覆盖 aqs方法的部分
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 新增了一个lock 的方法, 这个方法是用来 实现外层类的 lock接口的 lock方法的.
abstract void lock();
// 在这个方法中, 并没有重写 tryAcquire 方法, 估计要留给后面的子类
// 尝试释放
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // c 表示如果释放锁之后 是 0 , 也就是真正的释放锁了
free = true; // 那就锁空闲了
setExclusiveOwnerThread(null); //没有独占
}
setState(c); //重设 状态值
return free;
}
protected final boolean isHeldExclusively() { //得到当前独占的线程是否是当前线程
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
这里先忽略掉了 Sync 的 条件队列相关的东西 , 再来看一下 公布锁 和 非公平锁 是怎么实现的
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 正常抢锁
final void lock() {
acquire(1);
}
// 确实在这里 进行了 具体的实现 (对于 公平锁来说, 不存在提前抢锁)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 所以只有 当前没人抢锁, 且没有线程排队 , 本线程才回去抢锁
if (c == 0) {
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; //否则就是抢不到锁
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 正常抢锁前, 直接使用 cas 抢一下
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
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) {
// 如果当前是空的, 直接使用cas 抢一下试试, 插个队, 插到就好了
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;
}
}
这个例子告诉我们… 只要看懂了 AQS , 实现一个自定义的锁, 感觉还是挺简单的
趁热, 我们开始下一个 话题 条件队列
条件队列
条件队列, 就是 node 需要等待某个事情发生, 才会进行
坐标位于 : AQS 这个类中的 public class ConditionObject
首先是 看它 的基本成员属性 (我觉得学习一个类的最后方法 就是先看它有啥属性 , 猜一猜也行)
// Node 依然是 使用的 AQS 的 Node 对象 , 同样记录了 一个头, 和一个尾 , 那么是双向链表 还是 单向链表呢? 这个留着先
private transient Node firstWaiter;
private transient Node lastWaiter;
/** Mode meaning to reinterrupt on exit from wait */
// 这边有2个特殊的含义 ... 这个标识意思是 退出了 wait状态 之后 要恢复中断
private static final int REINTERRUPT = 1;
/** Mode meaning to throw InterruptedException on exit from wait */
// 这个标识 要抛出中断异常
private static final int THROW_IE = -1;
// 来看一个最简单的 加入 的方法...
private Node addConditionWaiter() {
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) { // 在这个条件队列里面, 如果 node 的状态 不是 Condition , 那么 就相当于取消了. // 这就意味着, 在等待队列中, 只可能是condition
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node; // 这里! 看清了, 使用的是Node 的 nextWaiter节点, 也就是说在 条件队列里面使用的是单项链表
lastWaiter = node;
return node;
}
// 这个方法 就是一个简单的全局扫描, 清理掉 所有 取消的等待节点
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
小小总结 :
- 通过对基础成员的分析, 我们得出了 条件队列 里面的 元素, 是呈现一种 单向链表 进行的
Condition.await() 方法
对于ConditionObject 来说, 有很多个await方法, 这些await 极大的丰富了…锁世界
具体的有
- void await()
- boolean await(long time, TimeUnit unit) : 对于这个方法来说 , 返回值如果为true , 那就是在规定的时间内抢到了锁, 如果为false , 就是在之后抢到的锁
- long awaitNanos(long nanosTimeout) : 这个和 上面的方法几乎相同, 纳秒级单位 : nb (返回值 负数 就是超时了, 正数 表示还多出了多少纳秒)
- void awaitUninterruptibly() (就这个不会响应中断)
- boolean awaitUntil(Date deadline) (绝对日期 , 到这个时间之前…) 返回值 同 await
和上文同样的道理, 看完 基础的成员变量之后, 我们来看一下 public 的方法… (一个类 我们只能使用它的public方法, 相当于一个类的入口)
// await() 方法
首先我们可以来看await方法… 这个命名 应该是用来区分 object.wait()的方法
它 也就是 来模拟 object.wait() 方法 的
我们想一想, synchronize 也需要一个 monitor 来作为监视器, 这样才可以调用 monitor的wait方法
同理. 如果 要使用 Condition . await 方法, 那么必然也需要先获得锁
我们这里来思考一个问题 ? 为什么 在条件队列里面, 好像所有的操作, 都没有使用cas , 或者 是加锁什么的? 会不会遇到并发问题呢?
答案是否定的 … DougLea大神 早就想到了这个问题, 或者根本不存在这个问题, 因为 要对条件队列进行操作, 必须是获得锁的…没有锁 直接报错了 . 所以对于 条件队列 而言, 永远都是单线程在操作它. 所以不存在并发的问题
/**
* Implements interruptible condition wait.
* <ol>
* <li> If current thread is interrupted, throw InterruptedException.
* <li> Save lock state returned by {@link #getState}.
* <li> Invoke {@link #release} with saved state as argument,
* throwing IllegalMonitorStateException if it fails.
* <li> Block until signalled or interrupted.
* <li> Reacquire by invoking specialized version of
* {@link #acquire} with saved state as argument.
* <li> If interrupted while blocked in step 4, throw InterruptedException.
* </ol>
+ ++ ++++++++++++++++++++++++++++ 这边对这一段进行一个翻译
大概就是
----- 如果当前线程已经被中断了, 那就抛出中断异常
----- 保存await之前的状态 (为了以后的恢复 (而且想要await 必须持有锁, 那就意味着现在的状态 就是线程当前的状态))
----- 调用 release 来保存状态 , 如果失败, 抛出异常
----- 之后就阻塞线程, 直到 被signalled 或者 中断
----- 重新调用acquire 来获得释放锁之前的状态
----- 如果在第四步已经中断了, 那就抛出中断异常
++++++++++++++++++
基本说完了如何实现的逻辑... 我们来看一看如何具体实现的吧
*/
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter(); //加入等待队列
int savedState = fullyRelease(node); //释放掉持有的所有 凭证, 这个状态就是凭证
int interruptMode = 0; // 用来保存上文所说的中断
while (!isOnSyncQueue(node)) { //判断当前node 是不是在 阻塞队列中 不在就进入语句块
LockSupport.park(this); // 其实不在阻塞队列 就意味着 还没唤醒, 这个时候就简单的挂起当前线程
//醒来之后看看是不是因为中断. 这个方法里面的逻辑 一会说
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//到这里, 能够确保的是 该node 已经进入了 阻塞队列... 那么就去进行一个抢锁
// 注意(acquireQueued的返回值 不是抢到了锁, 而是否是中断)
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT; //如果是中断的... (可能是执行方法之后中断的, 也可能是signal之后中断的)
// 不管这样的中断, 我都设置成 REINTERRUPT , (这个标识 signal 之后中断的)
// 全局清理已经取消的其他等待者
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果是因为中断, 那就根据需求, 要么抛出中断异常, 要么恢复中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode); //其实只有在signalled 之前 , 才会抛出异常
}
// 这个方法 是在 aqs 里面的 (就是字面意思, 释放所有的锁 (如果这个锁可以重入的话))
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
// aqs.isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null) // 如果是等待的..或者是在head的位置..那么肯定不在阻塞队列
return false;
if (node.next != null) // If has successor, it must be on queue ,如果有后继者...那一定是在阻塞队列里面了
return true;
return findNodeFromTail(node); //全局寻找, 从后往前
}
//aqs.findNodeFromTail
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) { // 做一个全局的寻找
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
到这里为止, 基本知道了一下 await 的具体逻辑, 不得不说 这是个很复杂的方法…(感觉)
- 首先一个前提, 获得锁之后 才能够调用await方法. (意味着能够调用await方法, 该node 是处在head的位置)
- 加入条件队列中, (提前判断一下是不是中断了)
- 调用fullyRelease去存储信息 以及 释放锁 (这个方法其实是依赖 tryRelease的, 所以依然需要重写tryRelease方法)
- 之后就进入park状态 (等待从park状态中恢复过来)
- 我觉得从park状态恢复过来之后的一些列操作 是比较复杂的
- 从park状态恢复过来, 第一件事情 就是判断一下是不是因为中断 导致从park状态恢复
- 如果是的话, 再看一下是在 得到了signal信号之前 还是之后
- 如果是之前的话, (这个情况下 需要手动把自己的node 放入到阻塞队列中) (标记需要抛出异常)
- 如果是之后的话, 也就是这个线程应该从await状态恢复过来, 但是还没有完成搬运的情况, 那么等搬运完成 (标记恢复中断)
- 如果是的话, 再看一下是在 得到了signal信号之前 还是之后
- 进行一个正常的抢锁环节, 抢到之后, 判断是不是中断了… 以及一个清理条件队列里面其他取消的node
- 如果中断的话 (根据 signal 前, 和signal 后)
- signal前 是抛出中断异常, 这是因为让应用程序去响应 await处的中断
- signal之后, 这就意味着 是 await 之后的代码出现了 中断, 这就恢复中断, 让具体的代码抛出中断异常 就完事了
来简单的看一下 遗漏的那个方法.
// conditionObject 下的方法
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
// aqs 下的方法
final boolean transferAfterCancelledWait(Node node) {
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
// 这个表示是在 signal之后的... (之后让其他的代码去响应这次中断)
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
//conditionObject 下的方法
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt(); // 说明在逻辑上 await方法 已经被激活了, 那就让应用程序之后的代码 响应这次中断信号
}
Condition.signal()方法
有2个方法
- void signal() (对应object.notify)
- void signalAll() 对应 notifyall
// 如果看上去十分的简单
public final void signal() {
if (!isHeldExclusively()) // 先判断一下是不是获得锁了. (这里我思考了一下 , 为什么await方法里, 并没有开头这样的东西) 而是 需要在 tryAcquire 方法中 自己去主动的判断一下是不是 获得了锁 ? 这个问题 不晓得...
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 如果这个 条件队列里面是有线程在等待的, 那就进行唤醒操作 . 也就是 singal
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 对头节点进行一个释放 , 如果释放之后, 下一个是null , 意味着这是最后一个节点 , 让tail = null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null; // 帮助gc , 让 first 游离
//如果唤醒失败的话, 就进行一个持续的自旋, 直到空为止
} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
// 尝试进行唤醒
final boolean transferForSignal(Node node) {
// 当前节点的状态 居然不是Condition? 那就说明取消了呀!
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; // 失败
Node p = enq(node); //不然就把它加入阻塞队列. 注意了 : enq 的返回值 是加入队列的前一个节点
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前一个节点 是取消状态的, 或者 cas的时候 发生失败 ?
// 什么时候会发生失败? 在判断的时候 ws 进行了取消? 应该是吧
// 那就对node的节点进行unpark ( 注意 : 这里是 其他的线程 对 node的线程进行的unpark)
// node 节点 还停留在 await方法中呢!!!!!!!!!!!!!!!!!! 这个很重要 (否则老乱了)
LockSupport.unpark(node.thread);
// 表示唤醒成功了!
return true;
}
小总结 :
- 这个唤醒的逻辑确实不是很复杂
- 首先判断一下是不是得到锁了之后进行的singal调用
- 在条件队列中选择出一个不是取消状态的node , 把它放进 阻塞队列, 然后如果它的前一个node 是取消状态的…那就唤醒这个线程
这边注意一个问题哦 : enq 方法中 并没有把 cancel 状态的node 请出 队列
acquireQueued() 方法里面 的 shouldParkAfterFailedAcquire() 方法 才会把取消的节点请出队列
这也是如果 node 前面是 已经取消的节点, 那么一定要把 node 唤醒, 让它进行acquireQueued方法把 取消的节点进行请出
还有一个 signalAll 方法 就是遍历一下 条件队列的所有节点, 调用 transferForSignal(Node node) 方法
到这里… 我们基本知道了条件队列的使用 , 以及 条件队列 和 阻塞队列 如何转换
共享模式
最后一个是共享模式 , 也是AQS 里的 最后一块内容了
使用的底层存储和之前一样的, Node节点 , 话不多说, 直接来看 AQS 对 共享模式的 public 方法
请求共享逻辑, 以及排队原理, 和得到锁之后进行唤醒后置节点
// aqs的方法 ...
// 这个方法看上去十分简约... 首先进行一下获取共享 arg , 如果小于0 (估计是没获取到) , 那就调用 doAcquireShared方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// 这个方法 需要在子类中 自行实现, 也就是如果需要使用共享模式, 那么需要重写这个方法
// 关于这个方法的返回值是什么 . 我们可以看一下 源代码的注解
/**
@return a negative value on failure; zero if acquisition in shared
* mode succeeded but no subsequent shared-mode acquire can
* succeed; and a positive value if acquisition in shared
* mode succeeded and subsequent shared-mode acquires might
* also succeed, in which case a subsequent waiting thread
* must check availability. (Support for three different
* return values enables this method to be used in contexts
* where acquires only sometimes act exclusively.) Upon
* success, this object has been acquired.
大概意思就是 , 如果返回 <0 那么就是失败的
如果 = 0 , 那么之后 再次请求 就不会成功
如果 > 0 那么之后再次请求 还是可能成功的
**/
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
//如果失败的话, 就进入 doAcquireShared
// 吐槽一下...这个代码格式 都快刻进DNA里了.. 这和 独占模式 中的获取 ....几乎一模一样
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) { // 独占模式下...这里 和下面的判断是放一起的
// 独占模式 (if (p == head && tryAcquire().....))
int r = tryAcquireShared(arg);
if (r >= 0) {
// 这里稍微有点不太一样
setHeadAndPropagate(node, r); //如果成功获取之后, 就会进入这个方法
p.next = null; // help GC
if (interrupted) // 把这个 恢复中断的操作 放进这里来了...
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 这边还是一样的设计, 会陷入一个阻塞状态, 会把前一个节点设为SIGNAL
interrupted = true;
}
} finally {
if (failed) // 同样的安全网的措施, 如果整个代码 产生了异常, 就会把这个节点进行排除
cancelAcquire(node);
}
}
小停顿:
- 在这里, 已经基本的展示了 共享模式的请求方式… 看上去 几乎 和 独享模式的请求方式一模一样…
- 几乎同样的逻辑处理… 这里就不说了… 把这些方法进行展开. 看看细节
// 让我们来看看 这个参数, node 毫无疑问 是当前的 node 节点, propagate 是 tryAcquireShared 的返回值 , 而且是大于 0 的
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 这个判断就比较奇怪了.. 我追溯了一下 AQS 内部 对这个方法的调用, 调用之前都是判断propagate...这参数需要 > 0 , 那这样这个判断基本每次都进入....
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next; // 得到下一个节点...
if (s == null || s.isShared()) // 下一个节点为空 或者 是共享的
doReleaseShared(); // 疑点 ?为啥为null 也要调用这个方法.. 看看这个方法是这么进行的
}
}
private void doReleaseShared() {
// 对于这个方法, 我们能看出来, 它是一个自旋的设计, 而且出口只有一个, 那就是 head 没有发生变化的时候
// 首先来分析一下 head发生变化是什么情况. 我们看代码 中间 有个 unparkSuccessor的操作
// 根据上面方法进来 来看, 当前head 其实就是 当前的node . 也就是 只有在unpark 后面的节点的时候, head才有可能发生改变
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) { // 什么时候 当前的 状态会是 SIGNAL 呢? 很简单, 后面一个节点需要唤醒的时候
// 因为该节点是 共享模式的, 那就意味着 不会阻塞后面的节点...
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 交换失败 只有一种情况, 就是下面的unparkSuccessor 的另外的线程 进行了... 出现几率很小. 问题不大
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒h的下一个节点
}
// 怎么才能够进入这个分支呢? 假设一个情况. 2个线程同时进入这个循环中, 在上一个判断的时候, 一个线程快一点点, 进行了一次cas 把 h 的状态变成了 0 , 那么后一个线程只能进入到这个 地方, 把 h 的状态 设置成 可传播的, 这样如果还有同时进入的线程, 就会什么都不做 离开这个方法...
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 把该点设置为可传播的
continue; // loop on failed CAS
}
if (h == head) //有没有被修改..那么就赶紧撤离.
break;
}
}
总结 :
- 到这里 , 基本看完了共享模式下的 请求 锁的方式
- 依然是一个插队的设计, 先去 tryAcquireShared 一下, 看看能不能直接抢到, 如果能直接抢到, 那么就ok
- 抢不到的话, 进入一个排队的逻辑中去, 排完队之后, 依然去看一下 是不是在特殊位置(阻塞队列的第一个) 看看能不能抢一下锁
- 如果还是抢不到, 那只能陷入 park 状态了, 等待有缘线程 进行唤醒 (或者中断 )
- 中断的话 , 做的是一样的操作, 等到拿到锁之后 , 会进行中断恢复
- 正常被唤醒的话, 就会 会到 第2步 , 是一个自旋的操作
- 如果获得了锁 , 就会把当前head 设置成自己, 由于它是一个 共享模式 的节点, 所以 它还会去唤醒 在它后面排队的节点
主动进行共享模式的释放过程
对应方法, releaseShared , 这个方法 依赖于 tryReleaseShared , 所以 想要使用共享模式, 那就需要 覆盖 tryAcquireShared方法, 和 releaseShared方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared(); //这个方法我们也看过了... 就是释放能够释放锁的节点
return true;
}
return false;
}
大总结 :
- 这个过程十分的简单. 到这里为止, AQS的几乎所有的模块 都已经看完了, 其余的方法 都是一些简单的方法, 主要用途是用来监控AQS的一些方法.
- AQS 有2个大模式 : 独占模式, 和共享模式, 通常情况下 只使用一种 模式, 当然也可以两种模式一起使用
- 在AQS中 有2个重要的队列, 一个是阻塞队列 , 一个是条件队列.
- 在独占模式下 , 阻塞队列中 的所有 node 都将会串行化的执行 , 也就是同一时间 , 只会出现一个节点进行 实际的代码运行
- 在共享模式下 , 阻塞队列中的head节点 会对后面的节点进行唤醒
- 对于 条件队列而言 , 进入 条件队列的前提 是要在 head的位置, 这个时候 加入到条件队列里面进行释放锁的操作, park线程
- 等待 这个条件的发生, 让别的线程把这个node节点转移到 阻塞队列之中 , 对线程进行唤醒
- 对于node来说, 从 条件队列中 移动到 阻塞队列, 就意味着 条件以及发生, 成功的进入了 等待 轮到本node 就可以运行了
- …感觉AQS 也就这么一回事…emmm 细节实现是真的强. 勉强能看懂.
如果有人能看到这里… 感谢一波, 顺便点个攒也行