在上一篇 并发编程之ReentrantLock源码分析_leon7199的博客-CSDN博客中,介绍了ReentrantLock的使用方法、基本原理和源码分析。看完上文之后,不知大家是否有什么疑问,我对源码有以下两个疑问
问题1:如何判断队列中是否有节点(线程)在排队
问题2:锁释放流程,unparkSuccessor方法中for循环为什么是从尾节点(tail)开始
我们先看一下问题1,判断队列是否有节点(线程)排队的源码:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
我们可以看到,满足 h != t &&((s = h.next) == null || s.thread != Thread.currentThread()) 表达式,表示队列有节点在排队,我们把表达式拆分一下
条件1: h != t,这个比较好理解,表示头节点 和尾节点指向不同的节点,说明队列不为空
条件2: (s = h.next) == null,将头节点的next节点赋值给临时节点s。头节点的next节点什么情况为null呢,是不是很疑惑,我们暂时先跳过,下面会详细讲解
条件3: s.thread != Thread.currentThread(),这个比较好理解,下一个节点的线程不等于当前线程,表示队列有其他线程
因此,只有条件1成立 并且(条件2 或 条件3 成立)时,表示队列中有节点排队。那么在什么情况下,条件1和条件2同时成立,也就是头节点和尾节点不相等,并且头节点的下一个节点为null呢?我们先回顾一下节点加入队列的源码吧,答案就在其中:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//入队
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在第一篇介绍ReentrantLock文章的基础上,我们可以看到,当第一个排队的节点调用addWaiter方法加入队列时,执行第一步,首先会通过new Node()创建一个空节点,并且head和tail都指向空节点,如下图所示
当执行到第二步,线程t1执行到enq()方法中 t.next = node 语句时,让出cpu,给其他线程执行;此时head指向Node0,tail指向Node1,Node1的prev指向Node0,还没来得及将之前的尾节点Node0的next节点指向Node1时,让出了cpu。
head=Node0,tail=Node1
Node1.prev=Node0, Node0.next=null
此时,条件1 h != t 成立,并且条件2 (s = h.next) == null 成立。
总结:在并发情况下,会出现 (s = h.next) == null 的情况。
脑袋突然闪过一句话what the fxxk,源码的作者DougLea到底是怎么样想到这种情况的,测试都不一定能复现出这种情况,作者却能想到,思维太严谨了。
我们继续看问题2,源码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
//如果头节点不为空,并且waitStatus != 0
if (h != null && h.waitStatus != 0)
//调用unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 设置 head 节点状态为 0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
可以看到,unparkSuccessor()方法的入参是头节点head。将head的next节点赋值给临时节点s。
s.waitStatus >0比较好理解,表示此节点为cancelled状态,而队列中可能有多个节点已经为cancelled状态,所以都要过滤掉,因此要从尾节点tail进行遍历,找到距离head最近的一个waitStatus<0的节点。
当head的next节点s为空时,流程不是可以结束了吗,为什么还要从尾节点遍历,寻找要唤醒的节点。
其实,这和我们问题1产生的场景一样,当并发情况下,head.next==null,而队列中是有线程在排队的,head节点和tail节点指向如下所示
head=Node0,tail=Node1
Node1.prev=Node0, Node0.next=null
所以当( s == null || s.waitStatus > 0)时,要从尾节点tail进行遍历,找到距离head最近的一个waitStatus<0的节点唤醒。