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

上篇我们讲了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点不同:

  • doAcquireShared方法直接将中断响应掉了,而acquireQueued只是返回中断标志,是否响应留在了acquire方法中;
  • doAcquireShared方法获取锁成功之后,除了将当前节点设置为head之外,还有个唤醒后继节点的操作,即setHeadAndPropagate方法。

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种情景:

  • 第1种情景

线程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,跳出自旋。

  • 第2种情景

线程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个资源。

  • A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
  • B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
  • C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
  • A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
  • C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
  • B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
  • C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343