AQS之ReentrantReadWriteLock精讲分析上篇 (3)

以下的循环做的事情就是,在队列存在后继线程的情况下,唤醒后继线程;或者由于多线程同时释放共享锁由于处在中间过程,读到head节点等待状态为0的情况下,虽然不能unparkSuccessor,但为了保证唤醒能够正确稳固传递下去,设置节点状态为PROPAGATE。

这样的话获取锁的线程在执行setHeadAndPropagate时可以读到PROPAGATE,从而由获取锁的线程去释放后继等待线程。

在共享锁模式下,头节点就是持有共享锁的节点,在它释放共享锁后,它也应该唤醒它的后继节点,但是值得注意的是,我们在之前的setHeadAndPropagate方法中可能已经调用过该方法了,也就是说它可能会被同一个头节点调用两次,也有可能在我们从releaseShared方法中调用它时,当前的头节点已经易主了

private void doReleaseShared() { for (;;) { Node h = head; // 如果队列中存在后继线程 if (h != null && h != tail) { int ws = h.waitStatus; if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // loop to recheck cases unparkSuccessor(h); } // 如果h节点的状态为0,需要设置为PROPAGATE用以保证唤醒的传播 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 检查h是否仍然是head,如果不是的话需要再进行循环 if (h == head) // loop if head changed break; } }

在看该方法时,我们需要明确以下几个问题:

该方法有几处调用?

该方法有两处调用,一处在doAcquireShared方法的末尾,当线程成功获取到共享锁后,在一定条件下调用该方法;

一处在releaseShared方法中,当线程释放共享锁的时候调用

调用该方法的线程是谁?

在共享锁中,持有共享锁的线程可以有多个,这些线程都可以调用releaseShared方法释放锁;因为这些线程想要获得共享锁,则它们必然曾经成为过头节点,或者就是现在的头节点。所以如果是在releaseShared方法中调用的doReleaseShared,那么此时调用方法的线程可能已经不是头节点所代表的线程了,此时头节点可能已经被更换了好几次了

调用该方法的目的是什么?

无论是在doAcquireShared中调用,还是在releaseShared方法中调用,该方法的目的都是在当前共享锁是可获取的状态时,唤醒head节点的下一个节点。(看上去和独占锁唤醒下一个节点似乎一样),但是它们的一个重要的差别是在共享锁中,当头节点发生变化时,是会回到循环中再立即唤醒head节点的下一个节点的。

退出该方法的条件是什么

该方法是一个自旋操作,退出该方法的唯一办法是走最后的break语句

if (h == head) // loop if head changed break;

只有在当前head没有变的时候,才会退出,否则继续循环。为什么呢?
为了说明问题,这里我们假设目前sync queue队列中依次排列有

dummy node -> A -> B -> C -> D

现在假设A已经拿到了共享锁,则它将成为新的dummy node,

dummy node (A) -> B -> C -> D

此时,A线程会调用doReleaseShared,我们写做doReleaseShared[A],在该方法中将唤醒后继的节点B,它很快获得了共享锁,成为了新的头节点:

dummy node (B) -> C -> D

此时,B线程也会调用doReleaseShared,我们写做doReleaseShared[B],在该方法中将唤醒后继的节点C,但是别忘了,在doReleaseShared[B]调用的时候,doReleaseShared[A]还没运行结束呢,当它运行到if(h == head)时,发现头节点现在已经变了,所以它将继续回到for循环中,与此同时,doReleaseShared[B]也没闲着,它在执行过程中也进入到了for循环中
我们这里形成了一个doReleaseShared的调用循环,大量的线程在同时执行doReleaseShared,这极大地加速了唤醒后继节点的速度,提升了效率,同时该方法内部的CAS操作又保证了多个线程同时唤醒一个节点时,只有一个线程能操作成功

那如果这里doReleaseShared[A]执行结束时,节点B还没有成为新的头节点时,doReleaseShared[A]方法不就退出了吗?是的,但即使这样也没有关系因为它已经成功唤醒了线程B,即使doReleaseShared[A]退出了,当B线程成为新的头节点时doReleaseShared[B]就开始执行了,它也会负责唤醒后继节点的,这样即使变成这种每个节点只唤醒自己后继节点的模式,从功能上讲,最终也可以实现唤醒所有等待共享锁的节点的目的,只是效率上没有之前的快。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wppjjx.html