Java的AQS详解2--共享锁的获取及释放

2020-06-06  本文已影响0人  安中古天乐

上篇我们讲了Java的AQS详解1--独占锁的获取及释放,本篇接着讲共享锁的获取及释放。

加锁

共享锁加锁的方法入口为:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared(arg)尝试获取锁,由AQS的继承类实现。

若返回值为负,证明获取锁失败,紧接着执行doAcquireShared(arg)方法。

doAcquireShared

private void doAcquireShared(int arg) {
    // 将该线程封装成共享节点,并追加到同步队列中
    final Node node = addWaiter(Node.SHARED);
    // 失败标志
    boolean failed = true;
    try {
        // 中断标志
        boolean interrupted = false;
        for (;;) {
            // 获取node的前继节点
            final Node p = node.predecessor();
            // 若node的前继节点为head节点,则执行tryAcquireShared方法尝试获取锁(资源)
            if (p == head) {
                int r = tryAcquireShared(arg);
                // 若返回值>=0,表明获取锁成功
                if (r >= 0) {
                    // 将当前节点设置为head节点,并唤醒后继节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    // 如果中断标志位true,响应掉中断
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 若前继节点不为head节点或者前继节点为head,但tryAcquireShared获取锁失败
            // shouldParkAfterFailedAcquire自旋CAS将node的前继节点的状态设置为SIGNAL(-1),并返回true
            // parkAndCheckInterrupt将线程阻塞挂起,重新被唤醒后检查阻塞期间是否被中断过,将interrupted置为true
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 若线程异常,则放弃获取锁
        if (failed)
            cancelAcquire(node);
    }
}

可以看到,doAcquireShared方法和独占锁的acquireQueued方法逻辑类似,主要有2点不同:

setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {
    // 原有head节点备份
    Node h = head; 
    // 将当前节点设置为head
    setHead(node);
    
    // 若propagate>0(有剩余资源)或者原head节点为null或原head节点的状态值<0
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        // 获取node的后继节点
        Node s = node.next;
        // 若后继节点为null或者为共享节点,则执行doReleaseShared方法继续传递唤醒操作
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

doReleaseShared

private void doReleaseShared() {
    for (;;) {
        // 此时的head节点已经被替换为node节点了
        Node h = head;
        // 若head不为null且不是tail节点
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 若head节点状态为SIGNAL(-1),则自旋CAS将head节点的状态设置为0之后,才可以唤醒head结点的后继节点
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    // 执行下一次自旋
                    continue;
                unparkSuccessor(h);
            }
            // 若head节点状态为0,则自旋CAS将节点状态设置为PROPAGATE(-3)
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                // 执行下一次自旋
                continue;
        }
        // head指针在自旋期间未发生移动的话,跳出自旋
        if (h == head)
            break;
    }
}

为什么最后需要判断(h==head)才跳出自旋?

想象2种情景:

线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread2);

thread1自旋CAS将新的head节点(thread2)的状态由SIGNAL修改为0,然后去唤醒thread2的后继线程thread3,当执行到(h == head)时,假如thread3唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread3),

thread1自旋CAS将新的head节点(thread3)的状态由SIGNAL修改为0,然后去唤醒thread3的后继线程thread4......

直到某个被唤醒的线程因为获取不到锁(资源被用尽)执行shouldParkAfterFailedAcquire方法被阻塞挂起,head节点才没有发生改变,此时(h == head)返回true,跳出自旋。

线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后还未来得及将head指向自己,此时(h == head)返回true,thread1停止自旋;

thread2唤醒后将执行setHeadAndPropagate方法将head指向自己,并最终进到doReleaseShared方法的自旋中;

此时,线程thread2自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread3......

哈哈哈,是不是像thread1一样又面临了2种情景。

可以看到,整个唤醒后继节点的过程是不断嵌套,螺旋执行的,每个节点的线程都最大程度的尝试唤醒其可以唤醒的节点,而且每个线程都是唤醒的head的后继节点,head指针不断往后推进,则被唤醒尝试获取共享锁的线程越多,而新的线程一旦获取到锁,其又会执行到setHeadAndPropagate-->doReleaseShared的自旋中,加入到唤醒head后继节点的联盟大军中,直到无锁可获。

所以,整个唤醒后继节点的过程如果一场风暴一样,不得不惊叹这样的设计呀,最大程度的诠释了何为共享,就是"有肉一起吃,有酒一起喝"。

解锁

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

某线程执行tryReleaseShared方法成功后,会释放掉部分资源,然后执行doReleaseShared方法唤醒当前head节点的后继线程,来参与分享资源。

doReleaseShared方法前面陈述过了,这是个"唤醒风暴",它会唤醒所有可以唤醒的人来参与资源的分享。

整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源。

回顾整个共享锁加锁和解锁的过程,可以发现head指针至关重要,无论是加锁成功后执行setHeadAndPropagate方法进而执行doReleaseShared方法,还是线程解锁时直接执行doReleaseShared方法,其均是直接从当前队列的的head节点的后继节点开始"唤醒",而被唤醒的多个线程也是通过(h == head)判断来决定是否跳出"唤醒自旋"的。

最后,再次感叹,这个"唤醒风暴"设计得太赞了!!!

上一篇 下一篇

猜你喜欢

热点阅读