ReentrantLock和AQS源码解读系列二
公平锁的细节hasQueuedPredecessors
如果我们设置为公平锁,那么在尝试获取所之前会先判断队伍里有没有人在排队,对应的就是:
public final boolean hasQueuedPredecessors() {
Node h, s;
/**
* 头结点为空的话,说明队伍都没创建,更不用说排队了,可以尝试获取锁,直接返回false
*/
if ((h = head) != null) {
/**
* 如果头结点的下一个为空,说明只刚创建了队列,头尾都是同一个结点,没人排队,那下面的循环就不执行了,可以尝试获取锁,返回false
* 如果头结点的一下个不为空,说明有人排队了,但是要看下排队的那个人的状态,
* 如果状态是取消,那就要开始循环,从队尾开始,一直找到队头位置,尝试找出离队头最近的不是取消状态的结点
* 为什么是从队尾开始,而不是队头,预备知识二的文章有解析过。
*/
if ((s = h.next) == null || s.waitStatus > 0) {
s = null; // traverse in case of concurrent cancellation
for (Node p = tail; p != h && p != null; p = p.prev) {
if (p.waitStatus <= 0)
s = p;
}
}
/**
* 如果找到了那个结点,而且结点中的线程不是当前线程,说明别人在排队了,返回true,
* 如果找不到,就直接返回false,或者找到了,又正好是自己,当然可以获取锁,说明自己在队伍里,而且是被唤醒或者中断的,尝试获取锁的,所以也返回false,
*/
if (s != null && s.thread != Thread.currentThread())
return true;
}
return false;
}
基本上的情况我都注释了,主要还是注意认为有人的情况其实是认为有别人在排队,而不是自己,如果发现那个人是自己,那也不算有人排队,因为自己就是在队列里的,无非是醒来自旋获取锁而已。所以这个机制就避免了插队的可能性。
取消状态细节cancelAcquire
从代码上看,貌似只有发生了异常或者error,而且捕获的是所有的情况,会出现取消状态,其实另外就是用了tryLock超时获取锁,超时了也会取消:

具体代码:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
//释放线程引用
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;//寻找状态不为取消的前驱结点,可能是直接前驱,也可能是隔着好几个取消结点的前驱,作为新的前驱
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary, although with
// a possibility that a cancelled node may transiently remain
// reachable.
Node predNext = pred.next;//获取新的前驱结点的后继结点,可能是node,也可能是个取消结点
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;//把结点直接设置为取消
// If we are the tail, remove ourselves.//如果当结点是尾结点,且设置新的前驱结点为尾结点成功了,就把新的前驱结点的后继结点设置为null,因为是尾结点了
if (node == tail && compareAndSetTail(node, pred)) {
pred.compareAndSetNext(predNext, null);
} else {
// If successor needs signal, try to set pred's next-link 如果前驱不是头结点,且是有SIGNAL标记的或者是可以设置成SIGNAL的,且线程不为空的,尝试设置后继结点
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)//如果node结点后继结点不为空,且不为取消状态,新的前驱结点的后继结点就是node的下一个结点
pred.compareAndSetNext(predNext, next);//这里只设置了新前驱的后继,不过也不一定能设置成功,如果自旋设置有点浪费性能了
} else {
unparkSuccessor(node);//如果是头结点,唤醒node的后继
}
node.next = node; // help GC //node后继指向自己,断开引用,前驱不能设置,不然可能就断链了
}
}
其实就是如果出现一个结点要取消,找到该结点的非取消前驱结点,如果该前驱是头结点,那就唤醒该结点的后继,如果不是,那就把该前驱的后继改为该结点的后继,但是这里并没有把后继的前驱改为新前驱,后继的前驱还是指向node,这里感觉有点奇怪,不过这里修改都是用CAS,也就是可能多线程一起修改,肯定有失败的情况,如果自旋去修改,估计性能就低了,就算不修改其实也没关系,只要连接在,整个队伍的前驱后继信息不丢就行。
为了帮助理解,我还是把几种情况画下图好点。
如果取消的是尾结点
修改尾结点成功
也就是尾结点改了。

修改尾结点不成功
尾结点没改,不过没关系,后面新来的自然就把尾结点设置了。

如果取消的是不尾结点,新前驱是头结点
那就要唤醒后继结点`unparkSuccessor(node),最后就变成这样:

如果取消的是不尾结点,新前驱也不是头结点

当然前面的都是成功的情况,如果不成功,也没关系,源码里也没有说要自旋一定改了为止,其实这里就是能改最好,改不了,在后续结点阻塞的时候会去把前面取消的清除出去的,所以不用担心,如果在这里强制改了,可能性能不好,看shouldParkAfterFailedAcquire中的一段:

也就是这样,其实没关系,取消的结点不会影响队伍里了,而且到时候会被GC释放:

最后,如果取消的不是尾结点的话,就执行了node.next = node;,就只改了后继,前驱怎么没改,一般想着如果要删除,不应该连前驱也改成自己么,其实如果这里改了,就可能出现断链的情况,比如:

一旦出现这样情况,前驱找不到要唤醒的非取消后继了,后继结点也找不到要标记SIGNAL的非取消前驱了。
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。
本文深入探讨了ReentrantLock与AQS(AbstractQueuedSynchronizer)在公平锁模式下的工作原理,详细解析了hasQueuedPredecessors方法如何判断队列中有无其他线程等待,以及cancelAcquire方法在处理线程取消请求时的内部机制。通过代码分析和图解,帮助读者理解公平锁的实现细节。
1600

被折叠的 条评论
为什么被折叠?



