AQS : waitStatus = Propagate 的作用解析 以及读锁无法全获取问题
仅供参考
Propagate 的作用:
学习AQS的过程中,发现Propagate这个状态并没有被显示地使用
比如 if(ws == PROPAGATE) { 操作 }
读了一些博客,感觉都是讲的模模糊糊,于是直接看源码。
当然,下面这篇文章也需要读者对源码有一定了解,本文不贴大量源码,因为本文不是源码解析。
假设现在有一种情况:
头节点是一个独占模式下的节点(一般这个节点的线程占有了写锁),后续都是共享模式下的节点,共享的节点等待独占节点释放资源
过了一段时间,独占节点释放资源。共享节点调用setHeadAndPropagate把自己变成头节点,把刚才的独占节点挤出了队列。
假设setHeadAndPropagate的propagate参数大于0,也就是现在的头节点获取了共享资源,并且之后的节点也可以获取共享资源。
setHeadAndPropagate中会调用doReleaseShared,因为propagate大于0。
这里的语意很清晰,其实就是当前节点获取共享资源,如果返回的值大于0,表明下个节点的线程也能获取共享资源,所以当前节点调用doReleaseShared来唤醒下一个节点中的线程
接着向下看,下一个共享节点被上一个节点的doReleaseShared唤醒之后,同样也调用setHeadAndPropagate,假设这次setHeadAndPropagate的第二个参数是0,
也就是获取了共享资源,但是再下一个节点是没有资源可获取了。也就是最后一个节点不应该被唤醒。
在setHeadAndPropagate方法中,会保留旧头,也就是上图的old,在setHeadAndPropagate中调用doReleaseShared的条件是
以下任一满足即可
1.旧头是空 2.旧头的ws < 0 3.新头是空 4.新头的ws < 0 5.setHead的第二个参数大于0
我感觉1不太可能成立,因为old做为局部变量才刚刚被获取,根据Java内存模型,是从内存中获取到当前线程CPU的高速缓存中,而且函数栈帧中被引用的变量一般也不会被辣鸡回收机制回收。
2的话可能成立,因为doReleaseShared里会循环把h的ws设置成PROPAGATE状态,如果没有PROPAGATE,那就只能是0
3也不太可能
4很有可能,因为只要有后继,后继就会在shouldParkAfterFailedAcquire方法中把前一个节点的ws设置成SIGNAL(前提是前一个节点没被撤销)
5这里假设了,等于0,所以5不成立
那么,现在,新头无法调用doReleaseShared的条件取决于 2 和 4
情况A : 我们假设一种情况,破除4,让4不成立。并且我们假设PROPAGATE没用,也就是2中直接设置成0,而不是设置成PROPAGATE。
情况B : 我们假设一种情况,同样不让4成立,但是PROPAGATE保留,也就是2中可以设置成PROPAGATE。
如果情况A会造成本应该能获取共享资源的节点Hang住,而情况B可以让这个节点顺利获取该获取的资源。那么我们就证明了PROPAGATE的价值。
让4不成立的情况:
因为暂时无法获取资源,新入队的节点,ws 初始化是 0,如果后续有节点入队,那么ws可能会被后面的节点在shouldParkAfterFailedAcquire方法中设置成SIGNAL
也就是后面的节点委托前面的节点把自己唤醒。那么我们假设在setHeadAndPropagate方法中设立一个间隙a
也就是试了5个条件之后,我们假设在间隙a有新的节点入队,这时候才通过shouldParkAfterFailedAcquire把新头Head的ws设置成SIGNAL
这时候,新头的后面有了节点,但是间隙a之上的最后一个 h.waitStatus并不成立。(因为现在是0)。于是条件4不成立,那么还有条件2。
如果没有PROPAGATE,那么第三个条件 h.waitStatus(这里是旧头的ws) 也不成立,因为旧头的ws不会被设置成PROPAGATE,而是被设置成0。所以不会调用doReleaseShared,所以不会唤醒后续结点。
这不是很正常嘛?因为tryAcquireShared的返回值是0(setHeadAndPropagte的第二个参数),表示以后的节点没有共享资源可用了,就不应该调用doReleaseShared把后面的节点唤醒了啊
但是,共享获取模式下,即使节点的线程没有调用releaseShared,也是会出队的,只要是获取到了共享资源,那么出队了的节点的线程可能调用releaseShared,在releaseShared中会调用
doRelaseShared。而doRelaseShared是没有参数的,只是检查头节点的ws,如果头节点的ws 是SIGNAL 那就唤醒他的后继,并且把ws设置成0。如果ws是0,就把ws设置成PROPAGATE。
但是现在我们已经假设PROPAGATE没有用,删去了,于是只能检查是不是SIGNAL,如果是就唤醒头节点的后继。但是如果头节点已经唤醒了后继了,就像我们上面的情况,头节点的ws是0
那么调用releaseShared从而调用doReleaseShared就无事可做,而上面的五个条件检查那里,旧头的ws还是0,五个条件的if不成立,这种语意下,就是有不在队列里的线程释放了共享资源,当前节点应该唤醒后续节点,但是因为当前节点读取到的资源数为0,所以不会唤醒后续节点。
一种情况:
head -> A -> B
当前A线程在1处得到资源数 r = 0,0的含义是得到了资源,但是后续节点得不到,不应该A的唤醒后续节点B
假设在1和2之间有 线程 C 调用了 releaseShare 释放了资源(线程C是不在队列里的,且已经获取资源了的线程),那么理应让 r > 0,在 setHeadAndPropagate 中去唤醒后续节点(B)
因为 r 是局部变量。所以无法办到除了线程A之外的其他线程(比如C)改变r。但是 除A之外的其他线程(比如C)可以设置旧头的状态。
于是在 setHeadAndPropagate 中增加了 红色框条件 h.waitStatus < 0 也就是判断旧头的状态 来增加唤醒后续节点的情况
于是 ,队列外的已获取资源的线程可以通过 设置旧头的 状态为 PROPAGATE 来让 唤醒继续下去。
以上是 head 指向的还是 旧头,新头(当前节点)未成为 head 的情况
如果 head 指向了 新头,因为新头后面有节点的话,那么ws一般是 SIGNAL ,蓝色框条件成立,所以可以唤醒后续。
至于为什么 ws 不是 PROPAGATE 的时候 为什么是0 ,而不是 SIGNAL,是因为在 doReleaseShare的代码里,会把释放者的ws设置为0
也就是谁释放了 资源,唤醒后面的节点,谁的ws就应该被设置为0
因此上文中的旧头,ws 是 0,因为旧头唤醒了新头。
读锁无法完全获取:
假设这种情况:
一开始一个线程获取独占资源,后续进来了2个线程要求获取共享资源,一个要求独占资源,再一个要求共享资源。
如果这时候做为头节点的独占资源节点释放了独占资源,最后一个要求获取共享资源的节点是否能获取共享资源呢?
这种情况就像是依次 : 上写锁,上读锁,上读锁,上写锁,上读锁 ——>第一个写锁释放
这种情况下读锁是否都能全部获取到?答案是不能,只有前两个读锁可以,最后一个不行,因为AQS的队列机制,doReleaseShared释放到第二个独占节点的时候,发现他不是共享的
所以就不唤醒他,最后一个共享资源节点当然也没有办法被唤醒,因为他要依靠前一个节点唤醒自己,而前一个节点没醒,当然就不会唤醒自己了。
这就是一种:只要写锁释放了,其他线程要是能获取读锁,那么就都能获取读锁的假象。其实还是要看获取顺序的(入队顺序)