AQS(AbstractQueuedSynchronizer), 可以说的夸张点,并发包中的几乎所有类都是基于AQS的。

一起揭开AQS的面纱

1. 介绍

为依赖 FIFO阻塞队列阻塞锁相关同步器(semaphores, events等)的实现提供一个框架。 为那些依赖于原子state的同步器提供基础(CyclicBarrier、CountDownLatch等). 支持独占模式共享模式, 不同的模式需要实现不同的方法.

引用这位大佬的图 http://www.cnblogs.com/waterystone/p/4920797.html



这个图是AQS整体结构,从图中可以看到,AQS维护着一个阻塞队列(当线程获取资源失败时,就会进入该队列,等待,直到被唤醒), state是一个共享的资源。

2. 源码剖析

我们先看看AQS的类图,

内部类: Node,阻塞队列维护的元素;ConditionObject, 支持独占模式下的子类用作Condition实现, 后面会讲到。先看看Node的结构。

static final class Node {
/** 表明阻塞一个共享模式的结点 */
static final Node SHARED = new Node();
/** 表明阻塞一个独占模式的结点 */
static final Node EXCLUSIVE = null; /** waitStatus value to indicate thread has cancelled */
// 发生的情况: 中断或者超时
static final int CANCELLED = 1; /** 表明后继节点需要被唤醒 */
// 发生的情况: 后继结点被park()阻塞,当目前的结点释放或取消,必须要unpark它的后继结点
static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */
// 发生的情况: 当前结点在条件队列中(后面会讲解)
static final int CONDITION = -2; /**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
// 发生的情况: 执行releaseShared后,需要传播释放其他结点。
// 在doReleaseShared中进行设置。(后面讲解这个状态的必要性)
// 在共享模式下,才会用到
static final int PROPAGATE = -3; /** 结点的状态, 初始值为0*/
volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread;
....
}

tip: waitStatus > 0, 即 CANCELLED, 此时的结点不正常。

AQS使用了模板模式, 自主选择重新定义以下方法

  • tryAcquire - 独占模式
  • tryRelease - 独占模式
  • tryAcquireShared - 共享模式
  • tryReleaseShared - 共享模式
  • isHeldExclusively

调用这些方法,都会引发UnsupportedOperationException,后面的文章将通过其子类,来讲解它们的实现。

有了这些知识后,我们从下面这几个关键的共有方法入手去讲解AQS

  • acquire(int arg) - 独占模式
  • release(int arg) - 独占模式
  • acquireShared(int arg) - 共享模式
  • releaseShared(int arg) - 共享模式

2.1 acquire

独占模式下的获取资源,忽略中断。调用tryAcquire至少一次,若成功就返回。否则,将线程入队,并可能反复阻塞和接触阻塞,并调用tryAcquire直至成功。此方法可以用于实现 Lock.lock

    public final void acquire(int arg) {
// (2.1.1)
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

tryAcquire在后面文章结合子类进行分析。

代码(2.1.1)中,此时调用了tryAcquire,获取资源失败,返回false,继续执行后续方法。

addWaiter -- Queuing utilities

使用当前线程和给定mode, 新建一个Node,并且将新建Node入队

    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) {
// (2.1.2) 记住这个地方, 后面有个知识点会用到
node.prev = pred;
// (2.1.3)
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 插入node入队,初始化如果有必要
// 执行到这的情况:
// 1. 阻塞队列为空时,即tail == null
// 2. 代码(2.1.3), CAS失败
enq(node);
return node;
}

enq -- Queuing utilities

一直循环直到node入队成功

    private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 初始时,head与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;
}
}
}
}

经过了上面的操作,目前的线程已经加入了队尾,此时做的事情就是阻塞自己,等待资源释放并且获取,然后执行自己的操作

acquireQueued

以独占且不可中断的模式,获取已经在阻塞队列中的线程。若在等待时被中断,返回true

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 不断自旋直到获取到资源或被park
for (;;) {
final Node p = node.predecessor();
// 若node的前继结点是head,执行tryAcquire,尝试获取资源(可能刚好释放了资源,就可以不用阻塞)
// (2.1.4)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 检测是否需要被park,若需要就进行park,并返回在等待时是否被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 失败,发生的情况 1. 被中断 2. 超时.(其他方法调用该方法时,会发生)
if (failed)
cancelAcquire(node);
}
}

shouldParkAfterFailedAcquire -- Utilities for various versions of acquire

对获取资源失败的node,检测并获取结点。返回true,如果线程需要阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驱结点的状态为SIGNAL,此时就可以安心的park,等待前驱节点释放资源,然后唤醒自己
if (ws == Node.SIGNAL)
return true; // 此时ws为CANCELLED
if (ws > 0) {
/*
* 前驱结点状态为CANCELLED,跳过前驱结点并重试, 直到前驱节点不为CANCELLED
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus此时的值要么是0要么是PROPAGATE.
* 需要把前驱结点状态设置为SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

cancelAcquire -- Utilities for various versions of acquire

取消正在进行的获取资源的尝试

    private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return; node.thread = null; // Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev; // 获取过滤后的前驱的后驱节点
Node predNext = pred.next; node.waitStatus = Node.CANCELLED; // node为tail,就直接从队列中删除
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws; // 1. 前驱结点不是头结点,该结点不是老二
// 2. 满足以下任意一个条件:
// 2.1 此时的前驱结点状态为SIGNAL
// 2.2 此时的前驱结点状态为PROPAGATE或CONDITION,成功将状态设置为SIGNAL
// 3. 前驱节点的任务不为空
// 满足上面的条件,就将节点的前驱结点的next 指向 节点的后驱结点
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 {
// 此时结点刚好是老二;
// 代码(2.1.4) 可以看出,头结点要么是哨兵结点,要么是已经获取到资源的结点。
// 此时唤醒node的后驱结点,是为了防止后驱结点一直阻塞
unparkSuccessor(node);
} node.next = node; // help GC
}
}

unparkSuccessor

后驱结点存在一个在正在等待的结点,则唤醒它

private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;

        // 将当前的结点的waitStatus设置为0,失去SIGNAL、PROPAGATE的含义
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // (2.1.5)
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// [问题一] 为什么node的后驱结点为空,重新寻找是从后往前找
// 只要waitStatus <= 0, 都有机会被唤醒
for (Node t = tail; t != null && t != node; t = t.prev)
// (2.1.6)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}

2.1.1 acquire流程图

根据上面的分析,整一个流程图

2.1.2 小结

根据acquire流程图,一句话小结其流程,尝试获取资源,失败则将新建node(当前线程及独占模式)入队,检测自己是否是老二,是老二就再一次尝试获取资源,成功就返回中断标志,不是老二就设置为SIGNAL,park自己,然后安心等待被唤醒。

2.2 release

独占模式下的释放资源。解除阻塞一个或多个线程,当tryRelease返回true时。此方法可以用于实现 Lock.unlock

    public final boolean release(int arg) {
// tryRelease返回true,继续下面操作。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

2.2.1 release流程图

因为release(int arg)的主要流程是在tryReleaseunparkSuccessor中,但是tryRelease又是在子类中实现,所以该流程图也可以看作unparkSuccessor的流程图

2.2.2 小结

根据release流程图, 一句话小结其流程, 释放资源,唤醒后驱没有被取消的结点。

下面讲讲AQS的另一种模式,共享模式

2.3 acquireShared

共享模式下获取资源,忽略中断。至少调用tryAcquireShared一次,成功就返回。否则,线程将入队,可能会重复的阻塞和解除阻塞,直到调用tryAcquireShared成功。成功获取到资源,将会唤醒后驱结点,若资源满足。

    public final void acquireShared(int arg) {
// 在子类探究tryAcquireShared
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

对tryAcquireShared返回的参数,进行简单的介绍

  • 返回负数表示失败;
  • 返回零,随后的线程都不能获取到资源
  • 返回正数,随后的线程可以获取到资源

此时tryAcquireShared的返回值是小于零,表示获取资源失败,进行下一步处理

doAcquireShared

获取资源在不可中断的模式下

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) {
int r = tryAcquireShared(arg);
// 若前驱结点是头结点且刚好释放了,尝试获取资源
// (2.3.1)
if (r >= 0) {
// 将当前node设置为head,并且尝试唤醒node的后驱结点
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 跟独占模式的处理是一样的
// (2.3.2)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

setHeadAndPropagate

设置队列的头结点,达到条件就唤醒后面的结点.

private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/ // tips: h == null、(h = head) == null、s == null,是为了防止空指针的发生 // 执行到下面的情况:
// 1. propagate是tryAcquireShared返回的值。propagate(资源) > 0, 表示还有资源可以唤醒后面的结点。
// 否则,此时propagate = 0, 结合代码(2.3.1)
// 2. 旧的head的waitStatus < 0
// 旧的头结点释放了资源,执行了代码(2.3.4), 此时的waitStatus 为PROPAGATE(初始化为0)
// 3. 此时的head已经是当前结点了,后面若有结点(此时后面的结点在park),
// 那么新head的waitStatus肯定为SIGNAL, 结合代码(2.3.2) // 情况3,有可能会发生没必要的唤醒,因为此时去唤醒新head的后驱结点,但是此时还没
// 有释放资源,它后驱结点唤醒后,去获取资源,获取失败,又被park.
// 源码注释说到,虽然有争议,但是大多数情况下,需要去唤醒
// (2.3.3)
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}

代码(2.3.3)中, 为什么不只用propagate来判断是否唤醒后驱结点 [问题二]

doReleaseShared

共享模式下主要的释放资源的逻辑,唤醒后驱结点,确保线程不被挂起

 private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// (2.3.4)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// (2.3.5)
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

2.3.1 acquireShared的流程图

2.3.2 小结

一句话小结acquireShared的流程,尝试获取资源,若获取到资源,资源还有剩余就去继续唤醒后驱结点,若尝试获取资源失败,就park自己,等待被唤醒。 跟acquire相比,最大的区别就是,获取到资源acquireShared,还会去尝试唤醒其后驱结点

2.4 releaseShared

Releases in shared mode. Implemented by unblocking one or more threads if tryReleaseShared returns true.

    // 代码比较简单,就不分析了~
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

2.5 问题解答

[问题一] 为什么在唤醒后驱结点时,node的后驱结点为空,需要重新从后往前找

   // (2.1.2) 记住这个地方, 后面有个知识点会用到
node.prev = pred; // 1
// (2.1.3)
if (compareAndSetTail(pred, node)) { // 2
pred.next = node; // 3
return node;
}

仔细观察代码(2.1.2) 和(2.1.3), 此时添加结点相当于有三步,都不是原子性的,当执行到第二步时,就要唤醒后驱结点了,此时新增的结点只设置了前驱结点,队列设置了尾结点,但是没有设置后驱结点,如果从前往后查找的话,可能会丢失符合要求的结点。

[问题二],代码(2.3.3)中, 为什么不只用propagate来判断是否唤醒后驱结点。

请看这位大佬的博客 讲的非常详细

大致意思就是,

我们假设有A、B、C、D四个线程,前两个释放资源的线程,后两个是争抢资源的线程,此时只有A或B释放了资源,C、D才可以被唤醒,假设我们不看PROPAGTE

时刻一: A线程释放资源,执行代码 (2.3.4),head的waitStatus从SIGNAL(-1)变为了0

时刻二: C线程获取到资源,执行到代码(2.3.1), tryAcquireShared返回0

时刻三: B线程线程释放资源,执行代码 (2.3.4),因为此时未改变头结点,head的waitStatus为0,不能unparkSuccessor

时刻四: 此时C执行到代码(2.3.3),propagate(tryAcquireShared返回值)为0,C也不会去唤醒后驱结点,D线程就永远GG了

引用doReleaseShared注释中的一句话

status is set to PROPAGATE to ensure that upon release, propagation continues.

2.6 Condition

使用synchronized时,线程间通信使用wait, notify and notifyAll;而使用AQS实现的lock,线程间的通信就使用Condition中的await、signal...。Condition与Lock结合使用,同一个lock对应多个Condition。

public class ConditionObject implements Condition...

在AQS中,已经对Condition的方法进行了实现,子类想使用的话,只需要调用ConditionObject就行了

        final ConditionObject newCondition() {
return new ConditionObject();
}

本来想跟着源码走,简简单单介绍一下Condition,但是源码有几处细节,让我头秃,在网上搜索别人的博客,这篇博客解开了我的疑惑,对Condition介绍的非常详细,写的非常的完美~

根据大佬的博客,那我们下面简单讲解Condition的两个常用方法

  • await
  • signal

2.6.1 await & signal

导致目前线程阻塞直到被唤醒或中断;调用await后,会将当前的线程封装成node,加入到条件队列

        public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加结点到条件队列中
Node node = addConditionWaiter();
// 释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 不在同步队列中,就park
// (2.6.1)
while (!isOnSyncQueue(node)) {
// 执行到这,当前线程会立即挂起
LockSupport.park(this);
// 运行到这的话,情况: 1. signal 2. 中断
// 检验中断
// (2.6.2)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 第二部分
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

为了讲清楚代码(2.6.1)之后的逻辑,我们先看看signal的源码

将condition queue中等待最长的结点转移到sync queue中去,去争抢资源

        public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}

此时,执行signal的主要逻辑

        private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 将后驱结点置空
// (2.6.3)
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}

将condition queue中的一个结点转移到sync queue中去

    final boolean transferForSignal(Node node) {
// 这里表示当前已经被取消了。
// (2.6.4)
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false; // 将当前结点放入sync queue的末尾, 此时返回的是当前结点的前驱结点(一定要注意)
Node p = enq(node);
int ws = p.waitStatus;
// 前驱结点被取消,或者设置SIGNAL状态失败,就直接唤醒当前线程, 唤醒 = 有资格去竞争锁了
// enq返回的是前驱结点,我人傻了,看成是返回当前线程,就一直觉得逻辑不对。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}

这里signal的逻辑就讲完了,总结一下:

  1. condition queue中找出等待时间最长且未被取消的结点, 转移到sync queue的队尾去,同时要在condition queue中删除该结点
  2. 若在sync queue中的该结点的前驱结点被取消了或设置SIGNAL状态失败,要直接唤醒它,叫它去竞争锁。

signalAll的主要逻辑和signal是一样的,差别就是signalAll会把所有在condition queue中的结点转移到sync queue中去,并清空所有在condition queue中的结点,下面只贴一下signalAll的主要代码,

        private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
Node next = first.nextWaiter;
first.nextWaiter = null;
transferForSignal(first);
first = next;
} while (first != null);
}

我们再次回到await中去

        public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加结点到条件队列中
Node node = addConditionWaiter();
// 释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 不在同步队列中,就park
// (2.6.1)
while (!isOnSyncQueue(node)) {
// 执行到这,当前线程会立即挂起
LockSupport.park(this);
// 运行到这的话,情况: 1. signal 2. 中断
// 检验中断
// (2.6.2)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 第二部分
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

代码(2.6.1),继续讲解

isOnSyncQueue

一开始在条件队列中,现在在sync queue中等待重新获取资源,如果有这种的node就返回true

    final boolean isOnSyncQueue(Node node) {
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
return findNodeFromTail(node);
}

findNodeFromTail

从尾部找寻结点

    private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
// 结合下面的enq代码以及图思考一下,会明白此方法的意义的
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}

node.waitStatus == Node.CONDITION, 表示当前结点肯定在condition queue中。

为何是上面的那些条件?

我们上面看了转移到sync queue是用的enq方法

    private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
// (2.6.5)
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

结合代码(2.6.5),思考一下就知道isOnSyncQueue中条件设置的道理了,但是为何需要findNodeFromTail啦? 这是需要补充一个知识点了,在多个线程执行enq时,只有一个线程会设置为tail成功,其余的都只是设置prev,就可能会出现下面图片中的情况,'多个尾巴'一直不断自旋,最后会变成一个正常的链表。



此时线程的状态是,调用await后,将结点添加到条件队列中,且释放了自己持有的所有资源,并将自己park,此时等待被signal或者被中断。

        public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加结点到条件队列中
Node node = addConditionWaiter();
// 释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 不在同步队列中,就park
// (2.6.1)
while (!isOnSyncQueue(node)) {
// 执行到这,当前线程会立即挂起
LockSupport.park(this); /// 我们在这被挂起了
// 运行到这的话,情况: 1. signal 2. 中断
// 检验中断
// (2.6.2)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 第二部分
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}

执行到代码(2.6.2), 我们直到可能是被signal或被中断了。现在要解决的是,

  1. 是否被中断?
  2. 何时被中断?
  3. 中断如何处理?

我们带着这三个问题,继续出发~

补充一个小知识点,AQS定义了三种情况中断的值

  • THROW_IE, signal前被中断,要抛出InterruptedException
  • REINTERRUPT, signal后被中断
  • 0, 未被中断

关于REINTERRUPT这个中断,可以理解成,吃饭,吃完了但是还有一个菜没有上,问服务员,“如果没有炒,就不要了”,但是服务员告诉,菜已经下锅,所以这时候的中断就是REINTERRUPT,中断的太晚了。 -- 例子来自上面的那篇博客

我们继续看向代码(2.6.2)

checkInterruptWhileWaiting

        private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}

若被中断Thread.interrupted肯定为true

transferAfterCancelledWait

    final boolean transferAfterCancelledWait(Node node) {
// 中断情况一: 此时结点还在condition queue中,肯定是signal前就被中断了
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 这里还要加入到sync queue中,获取到锁才能抛出错,继续往后看
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
*/
// 中断情况二: 这里的情况就是signal后,node还没有执行enq,毕竟执行signal到执行enq还有几个步骤
// 此时就自旋,等待node转移到sync node中就行了
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}

上面的代码已经注明了,各种情况的发生时机,此时我们来到了await的第二部分~

        public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加结点到条件队列中
Node node = addConditionWaiter();
// 释放当前线程持有的锁
int savedState = fullyRelease(node);
int interruptMode = 0;
// 不在同步队列中,就park
// (2.6.1)
while (!isOnSyncQueue(node)) {
// 执行到这,当前线程会立即挂起
LockSupport.park(this); /// 我们在这被挂起了
// 运行到这的话,情况: 1. signal 2. 中断
// 检验中断
// (2.6.2)
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 第二部分
// acquireQueued获取到锁,并返回是否被中断。
// 有一种情况需要提一下,acquireQueued返回true,上面的interruptMode = 0,
// 表示signal后,在获取锁的时候被中断了
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT; // node还在condition上,说明是被取消了的node,清除它
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
// 对上面得到的interruptMode做出回应
reportInterruptAfterWait(interruptMode);
}

reportInterruptAfterWait

        private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}

2.6.2 await与signal的流程图


2.6.3 小结

先讲讲await的流程,看起流程图有点吓人,其实很多步骤是对不同时机的中断操作的记录

当await被执行,下面简单总结下await的流程

  1. 当前线程与CONDITION状态封装成node,加入到condition queue的末尾
  2. 释放线程之前获取的所有资源
  3. 若不在sync queue中,阻塞自己,等待被signal被中断
  4. 获取中断操作的时机,并记录表示何时中断的值(interruptMode)
  5. 不管是怎么被唤醒的,都要去竞争资源,直到获得资源为止
  6. 最后对不同的中断值,作出不同的操作

signal的流程就相对于简单一点

  1. 获取condition queue的头结点
  2. 检验是否被取消,若被取消,就获取头结点的后驱结点,以此类推;
  3. 将结点从condition queue中转移到sync queue中,而且会从condition queue中删除该节点
  4. 若结点插入sync queue,得到的前驱结点,被取消了,或者CAS前驱结点状态为SIGNAL

    失败,将直接unpark当前线程

3. 总结

Doug Lea,太秀了。AQS中有很多细枝末节的东西,只有自己去读了源码,理解为何这样做,你才会明白才会真正读懂AQS。

关于学习和写AQS文章时,看了一些博客,为我解答了自己的疑惑,慢慢加油,我也要向这些大佬看齐~

4. 参考

~~# 5. 面试中问题

这是我的一个想法,若我博客中写过的知识,在面试中有问到过,我会记录下来,没有就是目前还没遇到过

JAVA并发(1)-AQS(亿点细节)的更多相关文章

  1. JAVA并发-同步器AQS

    什么是AQS aqs全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLo ...

  2. 深入理解Java并发框架AQS系列(一):线程

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...

  3. 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.AQS框架简介 AQS诞生于Jdk1.5,在当时低效且功能单一的synchroni ...

  4. 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)

    深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...

  5. 【转】Java并发的AQS原理详解

    申明:此篇文章转载自:https://juejin.im/post/5c11d6376fb9a049e82b6253写的真的很棒,感谢老钱的分享. 打通 Java 任督二脉 —— 并发数据结构的基石 ...

  6. Java并发编程--AQS

    概述 抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态, ...

  7. 深入理解Java并发类——AQS

    目录 什么是AQS 为什么需要AQS AQS的核心思想 AQS的内部数据和方法 如何利用AQS实现同步结构 ReentrantLock对AQS的利用 尝试获取锁 获取锁失败,排队竞争 参考 什么是AQ ...

  8. 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock)

    一.前言 优秀的源码就在那里 经过了前面两章的铺垫,终于要切入正题了,本章也是整个AQS的核心之一 从本章开始,我们要精读AQS源码,在欣赏它的同时也要学会质疑它.当然本文不会带着大家逐行过源码(会有 ...

  9. Java并发框架——AQS中断的支持

    线程的定义给我们提供了并发执行多个任务的方式,大多数情况下我们会让每个任务都自行执行结束,这样能保证事务的一致性,但是有时我们希望在任务执行中取消任务,使线程停止.在java中要让线程安全.快速.可靠 ...

随机推荐

  1. P1149_火柴棒等式(JAVA语言)

    题目描述 给你n根火柴棍,你可以拼出多少个形如"A+B=C"的等式?等式中的A.B.C是用火柴棍拼出的整数(若该数非零,则最高位不能是0).用火柴棍拼数字0-90−9的拼法如图所示 ...

  2. c++ 反汇编 除法优化

    接上篇:<C++反汇编与逆向分析技术揭秘>--算术运算和赋值 printf("argc / 4 = %d\n", argc / 4); printf("arg ...

  3. Spring源码之注解扫描Component-scan

    本文主要介绍Spring的component-scan标签,了解spring是如何实现扫描注解进行bean的注册,主要实现实在 NamespaceHandler, NamespaceHandlerSu ...

  4. 配置docker的pdflatex环境

    技术背景 Latex在文档撰写方面是不可或缺的工具,尤其是在写文章方面,是必须要用到的文字排版工具.但是latex的环境部署并不是一个特别人性化的操作,尤其是在各种不同的平台上操作是完全不一样的,还经 ...

  5. HarmonyOS三方件开发指南(15)-LoadingView功能介绍

    目录: 1. LoadingView组件功能介绍2. Lottie使用方法3. Lottie开发实现4.<HarmonyOS三方件开发指南>系列文章合集 1. LoadingView组件功 ...

  6. 微信二维码引擎OpenCV开源研究

    <微信二维码引擎OpenCV开源研究> 一.编译和Test测试        opencv_wechat_qrcode的编译需要同时下载opencv(https://github.com/ ...

  7. Jmeter(四十) - 从入门到精通进阶篇 - Jmeter配置文件的刨根问底 - 中篇(详解教程)

    1.简介 为什么宏哥要对Jmeter的配置文件进行一下讲解了,因为有的童鞋或者小伙伴在测试中遇到一些需要修改配置文件的问题不是很清楚也不是很懂,就算修改了也是模模糊糊的.更有甚者觉得那是禁地神圣不可轻 ...

  8. (5)MySQL进阶篇SQL优化(优化数据库对象)

    1.概述 在数据库设计过程中,用户可能会经常遇到这种问题:是否应该把所有表都按照第三范式来设计?表里面的字段到底改设置为多大长度合适?这些问题虽然很小,但是如果设计不当则可能会给将来的应用带来很多的性 ...

  9. maven中心仓库OSSRH使用简介

    目录 简介 为什么使用中心仓库 发布到中心仓库前的准备工作 使用OSSRH 使用Sonatype创建ticket 中央仓库中的组件要求 提供Javadoc 和源代码 使用GPG/PGP给文件签名 Me ...

  10. 02-MySQL主要配置文件

    一.二进制日志log-bin 作用:主从复制 二.错误日志 log-err 默认关闭,记录严重的警告和错误信息,每次启动和关闭的详细信息 三.慢查询日志log 默认关闭,记录查询的sql语句,如果开启 ...