一. Java并发编程的基石

AQS是Java并发编程的根底,Java类库提供的并发工具如Semaphore, CountDownLatch, CyclicBarrier, ReentrantLock, ReadWriteLock等等都是建设在AQS上的,依照Doug Lea的说法,AQS是一个并发根底框架,用户通过继承AQS并覆写tryAcquire()tryRelease()来表白他所心愿的信号量管制形式,其余的细节则齐全由这个并发框架实现。
实践总是拗口的,所以还是间接来看细节好啦。

二. State的含意

AQS中外围的字段天然是private volatile int state,在用户层面表白管制state的形式就是覆写tryAcquiretryRelease办法。
最天然的想法是将state看做残余信号量,那么只有在残余信号量为正的状况下tryAcquire能力胜利,大多数并发工具都是这么操作的,比方Semaphore,也有为了实现非凡的性能而在残余量为0的时候tryAcquire能力胜利,比方CountDownLatch
也就是说state的含意由具体所要实现的性能紧紧关联在一起,用户须要覆写tryAcquiretryRelease来表白他所心愿进行的state管制。

三. 结构一个不可重入锁

我想来想去,AQS的解说着实不适宜自底向上,所以还是用从顶向下的形式解说好了。
Java类库中的Semaphore对于state的解释很纯正,就是将它当做残余信号量,tryAcquire示意获取信号量,tryRelease示意开释信号量,这也合乎大家学操作系统课程时所接管的信号量PV概念。然而难堪的是,它应用的是AQSShare模式,这对于初学者的了解切实是不太敌对。
而应用Exclusive模式的ReentrantLock中解决可重入的代码又太多,覆盖了最外围的代码。所以我决定还是本人结构一个简略的不可重入锁好了,这样的锁天然是不适用于理论业务的,不过很适宜解说:-)
自定义锁命名为GLock,为了代码简洁也不实现Lock接口了,不然又要覆写一大堆没用的办法,咱们怎么简洁怎么来。
Java类库的并发工具应用AQS都是通过在外部持有一个继承AQS的子类来实现的,这个子类的命名约定俗成都是Sync,咱们也入乡随俗

public class GLock {    private final Sync sync;    public GLock() {        // 设置总信号量为1        sync = new Sync(1);    }        /**     * 获取锁     */    public void lock() {        // 获取一个信号量        sync.acquire(1);    }    /**     * 开释锁     */    public void unlock() {        // 开释一个信号量        sync.release(1);    }    static class Sync extends AbstractQueuedSynchronizer {        Sync(int permits) {            setState(permits);        }                @Override        protected boolean tryAcquire(int acquires) {            int c = getState();            if (c == 0)                return false;            return compareAndSetState(c, c - acquires);        }        @Override        protected boolean tryRelease(int releases) {            setState(getState() + releases);            return true;        }    }}

GLock的总信号量在初始化时设置为1,并且应用Exclusive独占模式操作AQS
GLock完满地展现了信号量PV的概念——将AQSstate字段作为残余信号量来对待,tryAcquire-获取信号量,tryRelease-开释信号量。

四. 进入Acquire

假如业务逻辑调用了GLock.lock()办法,那么大家都晓得要么是胜利了,能够执行后续的业务逻辑,要么就是被阻塞住了,直到期待一段时间别的线程开释了锁,至于期待多久就天晓得了。
咋看之下,GLock.lock()办法中调用的是sync.acquire(1),咱们写的tryAcquire办法用在哪了?咱们来看一下AQSacquire源码

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

是在acquire里回调了tryAcquire办法。
acquire办法中,存在以下状况

  1. tryAcquire胜利了,那么不须要执行acquireQueuedselfInterupt(),间接返回,示意胜利获取了信号量,对于GLock也即意味着第一次获取锁就胜利了
  2. 如果tryAcquire失败了,这种状况须要进入AQS中的期待队列,咱们须要先在队列中占个地位,这也是addWaiter办法要做的事件

五. AQS的期待队列

FIFO队列是AQS的外围,其实这是一个双向链表,链表中的结点由Node类型示意

static final class Node {    volatile int waitStatus;    volatile Node prev;    volatile Node next;    volatile Thread thread;}

AQS中蕴含volatile Node headvolatile Node tail两个字段别离指向表头和表尾
后面说到第一次tryAcquire没有胜利时,须要进入期待队列,addWaiter办法的作用就是在表尾插入结点,参数mode有两种取值——Shared共享模式和Exclusive独占模式,这里应用的是独占模式

private Node addWaiter(Node mode) {    Node node = new Node(Thread.currentThread(), mode);    enq(node);    return node;}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;            }        }    }}

能够看到,当队列为空时,必须先在队列头部插入一个空白结点,也就是说,只有队列被第一次应用,后续所有工夫,队列的头部肯定不为空
咱们假如此时GLock的信号量曾经被线程X给占用了,而当初有三个线程T1, T2, T3都尝试获取信号量,并且三个线程都刚刚完结调用enq()办法,那么此时AQS的期待队列看起来像是这样

请问在队列中插入结点后线程是立即休眠呢还是有别的操作?咱们持续看acquireQueued办法。
咱们来看下acquireQueued源码,简洁起见我删除了局部不影响解说的代码

final boolean acquireQueued(final Node node, int acquires) {    boolean interrupted = false;    for (; ; ) {        final Node p = node.predecessor();        if (p == head && tryAcquire(acquires)) {            setHead(node);            p.next = null; // help GC            failed = false;            return interrupted;        }        if (shouldParkAfterFailedAcquire(p, node) &&            parkAndCheckInterrupt())        {            interrupted = true;        }    }}

咱们发现acquireQueued()整体是一个有限循环,不停地去尝试获取信号量,直到获取胜利才return
然而获取信号量并不是在队列中的任何线程都能够进行的,咱们要特地留神这句代码if (p == head && tryAcquire()),只有在以后线程所处结点是整个队列第二个结点时(第一个是头结点,它是个哑结点)才可能尝试获取信号量,这阐明,任意时刻,队列中只有第一个线程有资格获取信号量
在咱们的例子中就是T1才有资格获取信号量,假如以后占用信号量的线程X的业务逻辑比较复杂,执行工夫较长,那么线程T1在执行第二次tryAcquire()时仍然没有获取到信号量(第一次是在acquire()中),就会进入到shouldParkAfterFailedAcquire()办法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {    int ws = pred.waitStatus;    if (ws == Node.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;}

这个pred前置结点就是头结点,依据结点中waitStatus值的不同,这个办法有不同的逻辑,上面咱们来看看waitStatus不同值的含意

六. 队列结点中的waitStatus

waitStatus有如下取值

命名取值含意
CANCELLED1结点中的线程曾经勾销期待
SIGNAL-1后继结点要求持有信号量的线程开释信号量时唤醒它
CONDITION-2这个值只在Condition中用到,咱们临时不关怀
PROPAGATE-3共享模式下,开释信号量的线程要求让期待队列中的线程尽可能地在进入休眠前取得信号量

CONDITION状态咱们不必管,在信号量PV操作中不会用到这个状态,所以咱们要钻研的就是默认初始化状态0, CANCELLED, SIGNALPROPAGATE,进一步地,以后从易于读者了解的角度思考,应用的是AQS独占模式,PROPAGATE临时也不须要思考
咱们以后在GLock中须要思考的有0, CANCELLEDSIGNAL三种结点状态。
还有很重要的一点是,头结点的waitStatus永远不会是CANCELLED!,所以咱们最终须要考查的只有0和SIGNAL两种状态.


桥豆麻袋!你说头结点不会是CANCELLED,那么为什么会有这样的代码?

if (ws > 0) {    do {        node.prev = pred = pred.prev;    } while (pred.waitStatus > 0);    pred.next = node;}

这是因为队列中的线程能够随时退出期待,所以当发现前驱结点的waitStatusCANCELLED时,这阐明搞不好本人有机会成为队列中第二个结点哦(走狗屎运了,头结点到本节点之间的所有线程全副放弃了),所以始终往前找,直到找到第一个没有退出的结点(可能是头结点,也可能是其余结点,毕竟大家都是正经人,谁会随随便便让他人踩狗屎运呢)
用图谈话就是这样


请读者再翻到上一节去看shouldParkAfterFailedAcquire()办法的源码

  1. 假如头结点的waitStatus曾经是SIGNAL了,那么就会返回true,对于acquireQueued()办法中的shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()这一行代码而言,就会继续执行parkAndCheckInterrupt()办法
private final boolean parkAndCheckInterrupt() {    LockSupport.park(this);    return Thread.interrupted();}

LockSupport.park()办法会让线程休眠,直到被其余线程唤醒

  1. 如果前驱结点曾经退出,则找到第一个没有退出的结点,而后返回false
  2. 前两种状况都不是,那么头结点的waitStatus肯定是0或者PROPAGATE(后面说过CONDITION值不会用在队列中),这两种值的解决办法都一样,将头结点的waitStatus设置为SIGNAL,示意本线程心愿取得信号量的那位兄弟在开释信号量时唤醒本人,因为本线程极有可能要进入休眠了

大家能够发现,除了第一种状况会让线程立即休眠外,其余的状况都会导致线程的执行流程又回到acquireQueued()中有限循环的结尾
对于状况2,如果所有前驱线程都退出了,那么以后线程就有资格调用tryAcquire,如果此时信号量被线程X开释了,那么就能取得锁了; 如果依然获取失败,则再次进入shouldParkAfterFailedAcquire()办法,将头结点状态置为SIGNAL,而后跟状况3一样,在休眠前执行最初一次tryAcquire

对于状况3,会最初一次调用tryAcquire,如果还是无奈失去信号量,就跟状况1一样,进入shouldParkAfterFailedAcquire()间接返回true,而后间接进入parkAndCheckInterrupt()办法中进入休眠,期待唤醒

七. 多余的话

尽管咱们当初是以AQS的独占模式为主题进行剖析的,然而我还是想顺带提一下无关共享模式的货色。
上一节的状况3,头结点的waitStatus为0和PROPAGATE时都不会立即休眠,而是会再多尝试一次tryAcquire,些微不同的是,创立结点时waitStatus主动初始化为0,waitStatus不会本人扭转,PROPAGATE须要共享模式下的其余线程来被动设置
其实这就是我之前对PROPAGATE含意的解释:共享模式下,开释信号量的线程要求让期待队列中的线程尽可能地在进入休眠前取得信号量
PROPAGATE这个值所要表白的语义我集体参详了很久很久,尽管源码正文里论述了作者心愿这个值所要实现的指标,然而十分难以了解,通过重复浏览源码我明确了PROPAGATE真正的含意——给线程多一次机会尝试tryAcquire
其实默认状态0也实现了同样的性能,但我认为PROPAGATE是共享模式中表白心愿升高队列线程进入休眠几率的一种显式语义

八. 线程的呐喊,终于逃离了休眠的天堂

咳咳,题目起过头了。这一节咱们来剖析,在acquireQueued()中调用tryAcquire时胜利获取了信号量时的后续逻辑。不论是什么状况,取得信号量的后续逻辑都是一样的。

明天先写到这,啃鸡爪去了