AQS共享模式setHeadAndPropagate方法及doReleaseShared方法详解

6 篇文章 0 订阅
5 篇文章 0 订阅

补充说明

共享模式与独占模式的区别就是对于后续节点的唤醒操作

独占模式下,只有线程释放锁后,去唤醒下一个节点

而共享模式下,有两个地方会有唤醒后继节点的操作,1.节点获取锁后,会检查是否还能再获取,可以的话则调用唤醒方法;2.线程释放锁时,会调用唤醒后继节点的方法

在具体分析代码前,还有一个逻辑要梳理一下,这个是之前一直卡住我的东西:

共享锁的唤醒操作,并不是同时发生的,虽然多个线程释放锁会同时调用doReleaseShared方法,但是具体的唤醒方法unparkSuccessor并不是同时发生的。

同一时刻,只有一个线程能争夺到唤醒节点的机会,被唤醒的节点在获取锁后,会将自己变更为头节点,然后去尝试唤醒下一个节点,当然,这时候之前释放锁的线程也可能来争夺这次的唤醒机会。

所以,其实只需要保证每次变更头节点时,唤醒操作能传播下去即可,并不是说每个线程释放锁时都一定能执行到唤醒操作,它只是能争夺节点的唤醒机会而已

源码分析

首先我们来贴出这两个方法的代码

private void setHeadAndPropagate(Node node, int propagate) {
    //获取队列头节点
    Node h = head;
    //将当前节点设置为头节点
    setHead(node);

    //如果propagate>0表示该资源还可以被获取
    //如果旧头节点为null或者旧头节点的状态小于0
    //如果新头节点为null或者新头节点的状态小于0
    if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
        //读读共享,读写互斥,写写互斥
        if (s == null || s.isShared()) {
            //唤醒后继节点
            doReleaseShared();
        }
    }
}
private void doReleaseShared() {
    for (; ; ) {
        Node h = head;
        if (h != null && h != tail) {
            //如果头节点不为空,且存在后继节点
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                //判断头节点状态是否为-1,如果不是,则继续循环
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    //判断头节点的状态是否为-1,不是的话,就等待下次循环
                    continue;
                }
                //如果头节点状态为-1,则将它更改为0,再来唤醒后继节点
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                //如果头节点状态为0,且未能将该节点的状态更改为-3,则继续下一次循环
                continue;
            }
        }
        if (h == head) {
            //如果头节点还未发生变化,则跳出循环
            break;
        }
    }
}

我们可以看到,这两个方法,相对于独占模式来说,要复杂很多,而且还引入了新的节点状态PROPAGETE(状态为-3)

其实,在最开始的时候,是没有这么复杂的,当时的代码如下:

private void setHeadAndPropagate(Node node, int propagate) {
    //将当前节点设置为头节点
    setHead(node);

    if (propagate > 0 && h.waitStatus != 0 ) {
        Node s = node.next;
        //获取当前节点的后继节点,如果它为null或者它是共享节点,则唤醒头节点的后继节点
        if (s == null || s.isShared()) {
            //唤醒后继节点
            doReleaseShared();
        }
    }
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            //头节点不为空,且状态不为0时,调用唤醒节点方法
            unparkSuccessor(h);
        }
        return true;
    }
    return false;
}

这个代码与独占模式就几乎差不多了,只是setHeadAndPropagate方法多了个唤醒后继节点的逻辑

至于为什么要改成上面那种繁琐的代码,据我在网上查找,是为了解决JAVA6之前的一个bug

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6801020

我们先来根据下图来仔细分析下以前的代码会出现什么问题,以及为什么引入PROPAGETE:

  1.  线程ThreadA释放锁,并调用unparkSuccessor来唤醒后继节点,此时头节点Node的状态会更改为0,且Node1会从阻塞中被唤醒
  2. Node1会尝试获取锁,一般会获取成功,而且会返回0(表示当前线程能加锁成功,但是暂时没有多余的资源让其他线程获取了)
  3. 极端情况下,此时发生线程切换,Node1线程让出执行CPU,ThreadB释放锁
  4. 但是,因为头节点Node的状态为0,所以ThreadB调用releaseShared并不能去唤醒后继节点
  5. 此时,Node1线程又获取CPU执行权限,然后调用setHeadAndPropagate方法,且propagate还是旧数据0
  6. Node1变为头节点,但是因为propagate小于0,所以Node1也不会去唤醒Node2节点
  7. 这样bug就产生了,明明共享锁可能被多个线程持有,但是此时后续的唤醒操作全部终止了,只有等到Node1线程释放锁时,才可能恢复,而且极端情况下,可能会再次出现上述的情况(其实也也就是需要保证唤醒操作的传播性,让其在有剩余的锁资源时可以一个个去获取)

我们再来分析下新代码,首先是setHeadAndPropagate,它的判断条件更改为

propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0
  • 当propagate>0,这说明还有剩余资源量可以加锁,直接去唤醒后继节点即可
  • 当propagate<0,则判断旧头节点Node的状态是否小于0(-1,-3),如果是则可以去唤醒后继节点(至于h==null,是不可能成立的条件,因为代码能执行到这里,头节点就不可能为空,而且h引用了头节点,头节点也不会被GC,所以h==null永远不成立。至于为什么要加这个判断,我也不清楚,网上说是为了防止空指针)
  • 以上条件都不满足时,则判断新头节点Node1的状态是否小于0,如果是则可以去唤醒后继节点(该条件会导致不必要的唤醒,比如ThreadA释放锁唤醒了Node1,Node1变为头节点,此时Node1状态是-1的,但是还没有任何线程释放锁,它也去做了唤醒操作)

相对于旧版来说,新版的条件写的是相当宽泛,只有一种情况它不会去执行唤醒操作:就是ThreadA唤醒了Node1节点,此时Node节点为0,然后Node1节点获取了锁,并且将自己更改为头节点,此时ThreadB释放锁,该线程获取的头节点是Node1,然后它执行了Node2的唤醒操作,此时Node1的状态也为0,这时Node1才无法执行到唤醒操作(但是,这时Node1也没必要执行唤醒操作了,因为Node2已经被唤醒了,等到Node2获取锁后自然会执行后续节点的唤醒操作,所以唤醒操作的传播并没有停止)

接下来是doReleaseShared,

  • 首先,它加了一个死循环
  • 当头节点Node状态为-1时,会先进行CAS更新头节点状态至0,再执行唤醒后继节点Node1的操作(主要是确保多个线程释放锁时,只有一个线程去唤醒Node1)
  • 当头节点Node状态为0时,会尝试将Node状态更新为-3
  • 如果顺利走到下面的逻辑,且头节点还未发生变化,则跳出循环
  • 否则,说明有节点获取了锁,且将自己更新为头节点了,而这些线程会再次循环尝试去唤醒新的头节点的后继节点(个人觉得有点不能理解,新的头节点也会去调用唤醒操作,能保证唤醒操作传递下去,这里还要继续下次循环,只能是认为它能更快的去执行唤醒操作了,虽然这个说法很牵强

我们再来回到上面那个bug

当Node1节点被唤醒,但是还未设置新的头节点时,此时头节点Node为0,线程ThreadB会将Node状态更新至-3。那么即使ThreadB无法唤醒Node2,Node1也可以在setHeadAndPropagate中去唤醒Node2,相当于ThreadB将唤醒操作传播出去了。

下面是doReleaseShared的流程图

总结

  • PROPAGETE状态是为了防止极端情况下,释放锁的线程无法唤醒后继节点,让获取锁的线程能把唤醒操作继续下去
  • 获取锁的线程和释放锁的线程,同一时刻只有一个线程能去唤醒头节点的后继节点,其他线程会跳出循环不做操作,当然如果线程还未跳出循环时,头节点就发生了变化,那么这些线程会再次争夺执行唤醒操作的机会
  • 一个头节点的后继节点只会被唤醒一次

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值