AbstractQueuedSynchronizer(aqs)中acquire和release的理解

Aqs有两种模式,一种独占模式,一种共享模式,他们获取资源的方法分别对应

acquire-release、acquireShared-releaseShared(见此博文 https://blog.csdn.net/a6822342/article/details/84875304

 

这篇文章我们来看看独占模式下的获取资源和释放资源的代码。

 

先来看acquire的源码

 

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

 

首先获取尝试获取资源(这个tryAcquire需要子类自己实现,可以参考java.util.concurrent.locks.ReentrantLock.FairSync这里面的实现去理解),如果获取到就直接返回了,如果没有获取到,就把该个线程看成阻塞队列里面的一个结点加入到队列中去,也就是addWaiter方法做的事情,然后就在队列里面等待获取资源,acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

 

下面先看看addWaiter的源码

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;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

这里有个难点,就是它首先会执行快速入队的代码,也就是if语句里面的代码,如果它符合要求,则快速入队成功,如果不行,那么就执行正常入队的方法enq。

疑点1:为什么执行快速入队的时候会有可能不成功呢?

疑点2:enq(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;
            }
        }
    }
}

关于以上两个疑问的解释,在该篇博客里面有写道,请各位看官移步:

https://www.jianshu.com/p/c806dd7f60bc

相信看了上面这篇博客,各位应该对addWaiter方法有一定的了解了。

 

下面我们来看看acquireQueued(Node, int)方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC,头结点直接置null
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)  
            cancelAcquire(node);
    }
}

我们已经知道该线程已经获取资源失败,被加到了阻塞队列的尾部,那么接下来就是进入等待休息状态,等到队列前面的线程都完成了自己的事情,把资源释放出来之后就可以获取在资源,然后干自己的事情了。

我们看acquireQueued方法里面的failed变量一开始设置为true,代表着没有获取到资源,Interrupted代表在等待的过程中是否被中断。

在for循环中,首先获得该线程的前一个结点,看它是不是头结点,如果是头结点的话,那么该线程作为阻塞队列里面的第二个结点就有资格去获取资源了,然后通过tryAcquire去看有没有获取到资源,如果该线程的前一个结点是头结点的话并且自己也获取到资源的话,则把自己设为头结点,把之前的头结点设置为null,使其阻塞队列中移除。然后failed设置为false,return interrupted则是表明自己在等待的过程中有没有被中断过。

 

等待过程的代码是:

if (shouldParkAfterFailedAcquire(p, node) &&
        parkAndCheckInterrupt())
        interrupted = true;
}

这里是通过shouldParkAfterFailedAcquire和parkAndCheckInterrupt来判断自己是不是可以进入休息状态了,如果在休息状态的时候被中断过,则把interrupted设置为true,然后当自己成为头结点的时候返回。

 

下面我们看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了些什么。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;//这一块应该会把队列里面的CANCEL状态的结点都给剔除
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        /*
        * 这里的状态除了是0就是PROPAGATE,则将前驱结点的状态设置为SIGNAL,
        * 但是返回的是false,不让其休息,可能是在这段时间内,已经处理到该结
        * 点的前驱结点了,如果是的话,它就不用休息了,下一步就轮到它获得资源了
        */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

这里需要看结点的waitstatus状态,这里说明一下:

waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

 

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,将其结点的waitStatus改为CANCELLED,进入该状态后的结点将不会再变化。具有该状态的线程永远不会再次阻塞。

SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点需要被唤醒,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。

CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition队列上。在传输之前,它不会用作同步队列节点,此时状态将设置为0.(此处使用此值与字段的其他用法无关,但可简化机制。)--代码中的直译

PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

0状态:值为0,代表初始化状态。

AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态

 

那么我们回来看代码

int ws = pred.waitStatus; 首先拿到前驱的状态,如果前驱的状态是singnal,那么该结点就可以去休息了,只要等前驱做完事情之后唤醒自己一下就可以了,如果前驱的状态大于0,则根据上面的状态表表示它是CANCELLED,所以需要去找前面有没有一个正常的状态,也就是下面这行代码的内容:node.prev = pred = pred.prev; pred指向pred的prev,然后当前结点的前驱指向pred的前驱(跳过了当前结点的前驱结点),直到找到这么一个结点,然后把该节点的next指向node,则中间的不符合状态的结点都会断链(如CANCEL),然后被GC

Else语句中的这个compareAndSetWaitStatus(pred, ws, Node.SIGNAL)是在前驱结点的状态的值大于0的情况下才执行的,将前驱结点的状态变为signal,这样当前驱结点拿到资源的时候,就会去唤醒当前结点,这样当前结点就可以放心去休息了。

如果前驱的当前状态小于0,这里的状态除了是0就是PROPAGATE,则将前驱结点的状态设置为SIGNAL,但是最后返回的是false,则代表着不让其休息,可能是在这段时间内,已经处理到该结//点的前驱结点了,如果是的话,它就不用休息了,下一步就轮到它获得资源了(个人猜测)。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

 

到此acquireQueued()方法分析完成,总结下该函数的具体流程:

 

1.结点进入队尾后,检查状态,找到安全休息点;

2.调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;

3.被唤醒后,看自己是不是有资格能拿到号(还在acquireQueued的自旋for循环里面)。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1

 

 

 

下面我们来看release方法。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

 

首先执行tryRelease方法,如果成功,则执行if条件语句里面的方法,唤醒下一个后继结点。

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

 

tryRelease的方法需要子类去实现(可以参考java.util.concurrent.locks.ReentranLock.Sync去理解),如果释放资源成功(独占模式一般都会成功),返回true,不成功返回false。

 

下面进if条件语句里面看一看,首先获得头结点,然后如果它不空且它的状态不为0(结点的初始化状态为0,不为0的状态在独占模式下要么为CANCEL和SINGNAL,CANCEL的话这种状态的结点应该是在acquire的时候给剔除了,因为shouldParkAfterFailedAcquire这个方法会判断当前结点的前驱结点,然后符合规则的置waitstatus为SINGNAL,不符合的则继续往前找,直到找到之后将该前驱结点的next指向当前结点,则中间的结点都会断链,然后被GC。SINGNAL状态代表着该结点的后续结点需要被唤醒)的话,就调用unparkSuccessor方法。下面来看看unparkSuccessor方法。

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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的状态,也就是release的node h。首先判断它是不是小于0,小于0则代表它的状态为SINGNAL,然后将其状态用CAS转为初始状态,因为node这个节点的线程释放了锁后续不需要做任何操作,接着去唤醒后继结点。

然后去找它的后继结点,如果它的状态是大于0,那么只能是CANCEL状态。或者它的结点为null,那么就从队列的最后面往前去找离当前结点最近的状态小于等于0的结点,然后去唤醒它。

 

参考文章:

https://www.cnblogs.com/waterystone/p/4920797.html

http://ifeve.com/juc-aqs-reentrantlock/

https://www.jianshu.com/p/6afaef97264a

 

到这里独占模式的acquire-release方法就已经分析完了,希望大家喜欢。有什么不足之处,欢迎指教~

 

猜你喜欢

转载自blog.csdn.net/a6822342/article/details/84839391