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

也就是后面的节点委托前面的节点把自己唤醒。那么我们假设在doReleaseShared方法中设立一个间隙a

也就是试了5个条件之后,调用doReleaseShared之前。我们假设在间隙a有新的节点入队,这时候才通过shouldParkAfterFailedAcquire把新头Head的ws设置成SIGNAL

这时候,新头的后面有了节点,但是间隙a之上的最后一个 h.waitStatus并不成立。(因为现在是0)。于是条件4不成立,那么还有条件2。

如果没有PROPAGATE,那么第三个条件 h.waitStatus(这里是旧头的ws) 也不成立,因为旧头的ws不会被设置成PROPAGATE,而是被设置成0。

这不是很正常嘛?因为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不成立,这种语意下,就是有节点释放了共享资源,但是后续节点还是无法获取共享资源

 导致不正确的资源分配。如果PROPAGATE存在,那么五个条件检查的if 那里的 旧头的ws < 0 会成立,(PROPAGTE = -3)。于是后续节点可以正确获取资源。

读锁无法完全获取:

  假设这种情况:

一开始一个线程获取独占资源,后续进来了2个线程要求获取共享资源,一个要求独占资源,再一个要求共享资源。

    

 如果这时候做为头节点的独占资源节点释放了独占资源,最后一个要求获取共享资源的节点是否能获取共享资源呢?

 这种情况就像是依次 : 上写锁,上读锁,上读锁,上写锁,上读锁 ——>第一个写锁释放 

 这种情况下读锁是否都能全部获取到?答案是不能,只有前两个读锁可以,最后一个不行,因为AQS的队列机制,doReleaseShared释放到第二个独占节点的时候,发现他不是共享的

 所以就不唤醒他,最后一个共享资源节点当然也没有办法被唤醒,因为他要依靠前一个节点唤醒自己,而前一个节点没醒,当然就不会唤醒自己了。

 这就是一种:只要写锁释放了,其他线程要是能获取读锁,那么就都能获取读锁的假象。其实还是要看获取顺序的(入队顺序)

原文地址:https://www.cnblogs.com/lqlqlq/p/12991275.html