- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
ReentrantLock的条件队列是实现“等待通知”机制的关键,之前在《java线程间通信:等待通知机制》一文中讲过了使用ReentrantLock实现多生产者、多消费者的案例,这个案例实际上在java源码的注释中已经给了,可以看Condition接口上的注释中相关的代码:
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
在文章《详解AQS二:ReentrantLock公平锁原理》中已经详细说了lock和unlock方法的实现原理,实际上就是利用了AQS队列实现阻塞、加锁、解锁。那么当lock、unlock方法中间夹杂着Condition的await、signal方法的调用,又发生了什么事情呢?
一切都要从ConditionObject类说起,它是Condition接口的实现类,lock.newCondition方法调用实际返回的就是ConditionObject类的实例。那么condition.await方法调用和condition.signal方法调用到底发生了什么呢?这实际上就是AQS队列中的Node元素和条件等待队列中元素的相互移动:
复习一下lock和unlock的句式 。
private final Lock lock=new ReentrantLock(); // 创建一个Lock接口实例
……
// 申请锁lock,如果发生竞争且竞争锁失败,则当前线程进入AQS队列等待
lock.lock();
try{
// 阻塞被释放后,当前线程执行临界区代码
//await、signal方法调用
……
}finally{
// 在finally块中释放锁,当前线程节点从AQS队列中移除
lock.unlock();
}
await方法、signal方法都必须在临界区代码中执行,await方法调用之后大家都猜到了:当前线程会进入条件等待队列等待。但是明明当前线程还在AQS队列中,还没有调用unlock方法呢,那条件等待队列中和AQS队列中是不是都会有当前线程的等待实例?答案是否定的。实际上调用sinal方法之后当前线程会创建新Node等待在Condition队列尾部,同时释放AQS锁,本身节点将会在AQS队列中被移除.
同样的,当调用signal方法之后,Condition队列中的节点会被移除,同时会创建新节点到AQS队列中等待重新抢占锁.
await方法执行时当前线程在AQS队列中必然是头部节点,也就是已经获取锁的节点。await方法执行的原理就是将节点从AQS队列中的头部挪到Condition队列的尾部,之后释放锁并唤醒AQS队列的下一个节点让其抢占锁.
public final void await() throws InterruptedException {
//如果当前线程发生了中断,就抛出中断异常
if (Thread.interrupted())
throw new InterruptedException();
//将当前线程封装Node节点并加到Condition等待队列
Node node = addConditionWaiter();
//释放锁并获取释放锁时查询的锁状态值(重入次数),以便于以后重新进入AQS队列使用
int savedState = fullyRelease(node);
//中断状态暂存标记
int interruptMode = 0;
/*
* 循环查询节点是否在AQS队列中,如果在AQS队列中表示已经重新进入AQS队列了,
* 这意味着有别的线程调用了signal方法唤醒了当前线程
*/
while (!isOnSyncQueue(node)) {
//不在AQS队列中,那就继续挂起等待
LockSupport.park(this);
//检查中断状态,防止由于发生中断导致LockSupport.park(this);失效
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//抢锁,如果在抢锁过程中发生了异常,则将中断标记设置为REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果是因中断导致的LockSupport.park(this);挂起失效,则遍历等待队列中的节点,删除无效节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//判定是否应当抛出中断异常还是仅仅恢复中断标记
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法的代码延续了AQS的代码风格,非常精简,但是信息量很大,每一行代码都值得推敲。总体来说,await方法干了以下的事情:
(1)封装当前线程为新Node节点,并添加到等待队列尾部 。
(2)AQS队列释放锁,并唤醒AQS同步队列中头部节点的后继节点 。
(3)执行while循环,将该节点的线程阻塞,直到该节点离开等待队列,进入AQS同步队列;或者检测到了中断异常结束while循环.
(4)退出while循环后,执行acquireQueued方法尝试获取锁.
(5)执行善后工作,遍历等待队列中的节点删除无效节点.
(6)最后根据中断发生的时机,signal方法调用前,就抛出异常;否则就设置下中断标记,是否抛出异常看业务代码处理.
/**
* 添加一个新的等待节点到等待队列
* @return 添加到等待队列中的节点对象
*/
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果最后一个节点取消等待了,则执行unlinkCancelledWaiters方法遍历等待队列删除无效节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
//封装当前线程为新Node节点,并且状态为CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;//将新节点放到队尾
lastWaiter = node;
return node;
}
这个方法很简单,就是创建了新节点Node对象,并且置为CONDTION状态表示等待在等待队列中;最后将其放到等待队列的队尾.
等待队列是一个单向队列,节点和节点之间使用nextWaiter指针链接.
值得一提的是,等待队列中节点Node类和AQS同步队列中的Node类是同一个类,之前在分析AQS时没使用到的节点状态Node.CONDITION以及属性nextWaiter在等待队列中全都用到了.
addConditionWaiter方法中比较让人费解的是这段代码:
Node t = lastWaiter;
// 如果最后一个节点取消等待了,则执行unlinkCancelledWaiters方法遍历等待队列删除无效节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
我们知道等待在等待队列中的节点必定是CONDITION状态,若不是这个状态,表示该节点已经取消等待了,那这时候就要将该节点从等待队列中移除。我们会有疑问,为什么该节点取消等待的时候不自己出队?感觉AQS所有的操作都似乎有“Lazy”的特性,包括AQS队列的初始化是在自旋入队的时候、AQS节点的出队在自旋抢占锁的时候。。都不是自己主动操作,而是“事件驱动”模式的.
节点入等待队列的时候可能会执行unlinkCancelledWaiters方法,该方法的作用是删除等待队列中已经取消等待的节点.
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null){
firstWaiter = next;
}else{
trail.nextWaiter = next;
}
if (next == null){
lastWaiter = trail;
}
}else{
trail = t;
}
t = next;
}
}
这段代码通过trail和t两个指针一前一后遍历整个等待队列,并剔除掉非Node.CONDITION状态的节点.
unlinkCancelledWaiters方法整体来说比较简单,该方法仅仅在当前线程持有锁的时候被调用,用于将取消等待的节点从等待队列中移除。在以下两个场景下该方法会被调用:
该方法需要避免在signal方法没有被调用的情况下产生无法被回收的垃圾节点,全遍历等待队列中的所有节点也是无奈之举,好在该方法被调用有前提:sinal方法没有被调用,而且等待节点因为一些原因取消了等待.
该方法遍历了等待队列中的所有节点,这样做有个好处:如果有大量的等待节点取消了等待,可能会导致“取消风暴”,这样一次遍历就取消所有指向垃圾节点的指针,可以避免多次重新遍历以提高运行效率.
节点加入等待队列以后会释放锁,并唤醒后继节点 。
final int fullyRelease(Node node) {
boolean failed = true;
try {
//获取锁状态,实际上就是当前线程的锁重入次数,以方便后续恢复
int savedState = getState();
//释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
ReentrantLock公平锁和非公平锁锁释放都调用的同一个方法:release方法,fullRelease方法一直没有被提及,因为它是为等待队列准备的方法。fullRelease方法内部会调用release方法.
fullRelease方法和release方法最大的区别就是release方法只会返回true/false表示释放锁成功了还是失败了,fullRelease方法则会先保存锁的状态,释放完锁以后会将该state返回给调用方await方法,用于以后恢复线程在AQS同步队列中的状态.
释放完成锁以后会执行while循环,不断执行isOnSyncQueue方法以判断节点是否在同步队列中,如果节点出现在了AQS同步队列中,说明节点已经被唤醒了,它可以重新尝试获取锁了.
final boolean isOnSyncQueue(Node node) {
/**
* 节点状态为CONDITION表示节点肯定在等待队列
* AQS同步队列是双向队列,等待在AQS同步队列中的所有节点prev都有值,
* 为null表示不在AQS同步队列中等待;反过来node.prev不为空并不表示node节点
* 一定在AQS同步队列。
*/
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
/**
* 等待队列中的节点通过nextWaiter指针相互链接,next指针用于AQS队列
* next指针不为空表示节点必定在AQS同步队列;反过来,node.next为空说明不了
* node节点不在AQS队列
*/
if (node.next != null)
return true;
//从尾部到头部遍历节点
return findNodeFromTail(node);
}
/**
* 从尾部到头部遍历AQS同步队列查找指定节点
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
得解释一下:
node.prev不为空说明不了node节点在AQS同步队列:
可以看下AQS的enq自旋入队方法 。
可以看到,在CAS操作入队之前,node.prev就已经设置了值了,而CAS可能会失败,所以就算node.prev有值,node节点也可能没有在队列中.
node.next为空说明不了node节点不在AQS同步队列:
这个很好理解,AQS同步队列tail节点的next指针是null.
最后一个问题是:为什么要从尾部到头部遍历AQS同步队列查找指定节点,而不是从头部到尾部查找?
一个重要的原因是当前节点刚加入AQS队列尾部的话,从尾部查找可能不会经历很多遍历就能查找到该节点,从尾部开始查找可以提高效率.
上一章节讲到了isOnSyncQueue方法,该方法判定当前节点是否在AQS同步队列中,如果在同步队列中,则结束while循环,开始执行acquireQueue方法抢占锁;否则执行LockSupport.park方法将线程阻塞。看似逻辑很简单,但是涉及到了中断相关的逻辑,比较复杂,需要单独拎出来掰扯掰扯.
public final void await() throws InterruptedException {
....省略之前的代码....
while (!isOnSyncQueue(node)) {
//不在AQS队列中,那就继续挂起等待
LockSupport.park(this);//①
//检查中断状态,防止由于发生中断导致LockSupport.park(this);失效
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//②
break;
}
//抢锁,如果在抢锁过程中发生了异常,则将中断标记设置为REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//如果是因中断导致的LockSupport.park(this);挂起失效,则遍历等待队列中的节点,删除无效节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
//判定是否应当抛出中断异常还是仅仅恢复中断标记
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
在①处执行了LockSupport.park代码,线程被阻塞挂起,那什么时候线程会被释放继续运行临界区代码呢?
有三个可能的原因:
signal方法被调用后,线程节点自旋进入AQS同步队列,正常等待机会获取锁之后被前驱节点线程调用LockSupport.unpark方法唤醒 。
有别的线程调用了signal方法,signal方法中可能会调用LockSupport.unpark方法直接唤醒线程 。
线程发生了中断,LockSupport.lock方法阻塞失效了.
可能很多人都会想到signal方法会唤醒线程,但是想不到可能发生的中断会让LockSupport.lock方法失效仍然会释放线程让代码继续运行。这也是为什么接下来的代码要紧接着检查线程中断.
确切的说是检查是否发生了中断,以及如果发生了中断,是在signal方法执行前发生了中断,还是signal方法执行后发生了中断.
/**
* 返回值有三种:
* 0:没有发生中断
* -1(THROW_IE):发生了中断,而且中断是在signal方法调用前发生的
* 1(REINTERRUPT):发生了中断,而且中断是在signal方法调用后发生的
*/
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
/**
* 如果需要,在取消等待后将节点转移到AQS同步队列。
* 如果节点是在被唤醒前取消等待的,则返回true,否则返回false。
* 只有发生了中断才会调用该方法。
*/
final boolean transferAfterCancelledWait(Node node) {
/*
* 这里的CAS操作如果成功,说明当前节点的状态是Node.CONDITION
* 也就是说,当前节点必定是在等待队列,更进一步,说明了当前中断
* 是在被唤醒之前发生的,因为如果调用了signal方法,当前节点必然
* 已经在AQS同步队列中,已经是状态0了。
*/
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
//自旋入队
enq(node);
return true;
}
/*
* 代码执行到这里,并不能说明signal方法已经执行完毕,有可能signal方法
* 正在执行中,Node状态已经更改成了0,但是enq方法还没执行完成,节点还在
* 疯狂自旋入队。所以这里要判断是否完成了enq方法,就要判定下当前节点是否
* 已经在AQS同步队列中。如果enq方法还没执行完,就让出CPU时间片,稍稍等待
* 下,通常enq方法会很快完成,所以不用担心这里会浪费CPU资源。
*/
while (!isOnSyncQueue(node))
Thread.yield();
//singal方法执行后发生的中断,返回false
return false;
}
checkInterruptWhileWaiting方法有三个返回值:0、1(REINTERRUPT)、-1(THROW_IE)。在这里,要特别注意Thread.interrupted()方法的调用,该方法在上一章节《详解AQS二:ReentrantLock公平锁原理》就说过了,它有个很重要的特点是会重置中断状态为false,并且返回中断状态。checkInterruptWhileWaiting的返回值1和-1都代表发生过中断。不管是1还是-1,都会停止while循环,中断值则会暂存在interruptMode变量中.
while (!isOnSyncQueue(node)) {
//不在AQS队列中,那就继续挂起等待
LockSupport.park(this);//①
//检查中断状态,防止由于发生中断导致LockSupport.park(this);失效
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//②
break;
}
中断循环以后,当前线程就开始抢锁,抢锁方法是acquireQueued方法,该方法已经在文章《详解AQS二:ReentrantLock公平锁原理》中讲过,不再赘述.
acquireQueued方法返回值是true/false,true表示发生了中断,false表示未发生中断,这个中断是在抢锁过程中发生的;在抢锁之前,while循环中调用了checkInterruptWhileWaiting方法,该方法调用了Thread.interrupted()方法重置了中断状态,正是为了不影响acquireQueued方法的调用.
我们分析下抢锁代码 。
//抢锁,如果在抢锁过程中发生了异常,则将中断标记设置为REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
这段代码执行了如下代码流程:
现在我们聚焦于interruptMode != THROW_IE这个判断条件,如果interruptMode不是THROW_IE,它会是什么值?只有可能是剩下的两种值:
0:在等待队列中未发生中断,但是acquireQueued方法执行的时候发生了中断 。
1(REINTERRUPT):在等待队列中等待的时候发生了中断,而且中断是在signal方法调用后发生的,而且之后在acquireQueued方法中抢锁的时候也发生了中断 。
在以上两种情况下,interruptMode会被修改成REINTERRUPT值(实际上第二种情况interruptMode的值已经是REINTERRUPT,重复修改也无妨) 。
这样,最终interruptMode一共只有可能有三种类型的值:
针对这三种类型的值,接下来会如何处理呢?
await方法最后一段代码:
//判定是否应当抛出中断异常还是仅仅恢复中断标记
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
这里首先判断了interruptMode是否是0,只有非0的值才会被处理,也就是说,只有异常类型才会被处理.
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
终于看到了对THROW_IE和REINTERRUPT类型的处理:
THROW_IE:抛出InterruptedException异常 。
REINTERRUPT:设置中断标记,这样如果在接下来的业务处理中出现了sleep等等待方法,将会抛出InterruptedException异常.
使用表格总结一下异常处理的各种情况 。
可以看到,是否要抛出异常还是设置中断标记,主要取决于节点在等待队列中发生中断的时机:
等待队列中在signal方法调用前发生的中断: 一定会抛出中断异常,不管acquireQueued方法有没有发生中断 。
等待队列中在signal方法调用后发生的中断: 一定会设置中断标记位,不管acquireQueued方法有没有发生中断 。
等待队列中没有发生中断:如果抢锁过程中发生了中断,就设置中断标记位;如果抢锁过程中没有发生中断,就什么都不做.
思考一下,为什么要这么做呢?
acquireQueued是一个“uninterruptible”的类,也就是说,它不会抛出中断异常,但是它会将中断反馈给调用方,让调用方决定该如何处理异常,那await就作为调用方,结合了在等待队列中是否发生异常做出了综合决定:一切看是否在等待队列中发生的异常种类,如果没有发生异常,就向AQS同步队列处理异常类型一样,让业务自己决定是否抛出异常;如果在等待队列中发生了异常:
await方法最后会判定node.nextWaiter是否为null,如果是null则遍历等待队列中的节点,删除无效节点 。
//如果是因中断导致的LockSupport.park(this);挂起失效,则遍历等待队列中的节点,删除无效节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
为什么会判断node.nextWaiter是否为null?想要知道这个答案,得看看signal方法的实现逻辑.
signal方法就是把线程节点从等待队列转移到AQS同步队列,之后就是AQS正常获取锁并被前置节点线程唤醒后继续执行同步代码块中await方法之后的的临界区代码的过程.
public final void signal() {
//如果当前锁的持有线程不是当前线程,就抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//firstWaiter是等待时间最久的线程,从它开始唤醒最公平
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
这个方法是个AQS的钩子方法,实现在ReentrantLock类中的Sync抽象类 。
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
这个方法特别简单,就是简单判定了下当前线程是否是持有锁的线程,什么情况下会发生不是持有锁的线程呢?明明当前线程正在运行中,它肯定是持有锁的线程啊,举个简单的例子如下所示:
/**
* @author kdyzm
* @date 2024/12/27
*/
public class AQSNotHolderThread {
public static void main(String[] args) {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
Condition con1 = lock1.newCondition();
Thread thread = new Thread(() -> {
lock1.lock();
try {
con1.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
}
});
thread.start();
lock2.lock();
try {
//在lock2的临界区代码使用lock1的con1,会抛出IllegalMonitorStateException异常
con1.signal();
} finally {
lock2.unlock();
}
}
}
以上代码会抛出IllegalMonitorStateException异常.
isHeldExclusively方法调用的目的就是要保证Condition方法的调用必须在对应的锁下的同步代码块中.
doSignal方法会从头部开始查找第一个没有被取消的节点,将其转移到AQS同步队列或者直接唤醒线程.
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
//不管后续有没有transferForSignal成功,都将nextWaiter指针置为null
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
在上面的代码中,first.nextWaiter = null;这块代码是亮点,虽然不经意的一行代码,却是解释第三章第五节:删除无效节点:unlinkCancelledWaiters 的关键,之前留下了一个疑问,在await方法的最后:
//如果是因中断导致的LockSupport.park(this);挂起失效,则遍历等待队列中的节点,删除无效节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
会判定node.nextWaiter是否为null,如果不是null,就执行unlinkCancelledWaiters方法清除无效节点.
从doSignal方法中可以看到,只要调用了调用了doSignal方法,节点的nextWaiter就会被置为null;那await方法中在LockSupport.park方法调用之后发现了node.nextWaiter竟然不为空,那就表示signal方法没有被调用,换句话说,是因为中断才使得LockSupport.park失效最终导致代码运行到这里的,在signal方法调用前发生了中断,那节点是取消等待状态,需要将节点从等待队列中移除出去,执行unlinkCancelledWaiters()方法就合理了.
该方法的主要目的是将线程节点从条件等待队列转移到AQS同步队列.
final boolean transferForSignal(Node node) {
/*
* 如果节点已经取消等待,则不再尝试转移节点到AQS同步队列
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 节点自旋转进入AQS同步队列,并返回前置节点
*/
Node p = enq(node);
int ws = p.waitStatus;
/**
* ws>0:前置节点状态是取消状态
* !compareAndSetWaitStatus(p, ws, Node.SIGNAL):设置前驱节点为Signal状态失败
*/
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);//唤醒节点线程
return true;
}
我们分析一下这段代码:
本来目标节点在await方法中已经是被LockSupport.park方法将线程阻塞了,enq方法完成之后,实际上该节点已经完成了入队,应该是是等待前驱节点唤醒它的状态,似乎等价于AQS的acquire方法 。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
中的acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法调用,然而真的等价吗?实际上不一定等价,其区别就是前驱节点的状态,由于目标节点是“先阻塞,后入队”,和正常进入AQS队列等待的线程“先入队,后阻塞”的流程是反着来的,所以它没有调用过shouldParkAfterFailedAcquire方法校正前驱节点状态:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire方法执行完成后会将取消状态的节点剔除,并且将前置节点状态修正为SIGNAL.
在transferForSignal方法中的代码 。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);//唤醒节点线程
实际上就是在快速验证前置节点状态,如果前置节点状态错误且无法修正,则将目标线程直接唤醒让其继续执行await方法中LockSupport.park方法之后的代码逻辑:
while (!isOnSyncQueue(node)) {
//不在AQS队列中,那就继续挂起等待
LockSupport.park(this);//①
//检查中断状态,防止由于发生中断导致LockSupport.park(this);失效
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//②
break;
}
//抢锁,如果在抢锁过程中发生了异常,则将中断标记设置为REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
由于在signal方法中已经入队,所以这里会结束while循环,执行下一个if语句中的acquireQueued(node, savedState),这个方法中如果获取锁失败,它会调用shouldParkAfterFailedAcquire方法:
/**
* 已经在队列中的线程通过独占模式获取锁的方法,该方法不受中断的影响。
* 该方法也同样用于等待在条件等待队列中的线程获取锁。
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
//如果前置节点是头部节点,当前节点就尝试抢占锁
if (p == head && tryAcquire(arg)) {
//抢锁成功后将抢锁节点设置为头结点
setHead(node);
//释放头结点利于GC
p.next = null;
failed = false;
return interrupted;
}
//如果应该阻塞等待就挂起线程进入阻塞状态
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
调用shouldParkAfterFailedAcquire方法就会重新校正前置节点状态为SIGNAL并且剔除掉前置节点中的取消状态的节点.
这样await方法将会在acquireQueued方法调用处阻塞,等待获取锁之后执行后续的临界区代码.
这样就完美闭环了.
https://blog.kdyzm.cn 。
END. 。
最后此篇关于详解AQS四:ReentrantLock条件队列原理的文章就讲到这里了,如果你想了解更多关于详解AQS四:ReentrantLock条件队列原理的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
本文全面深入地探讨了Docker容器通信技术,从基础概念、网络模型、核心组件到实战应用。详细介绍了不同网络模式及其实现,提供了容器通信的技术细节和实用案例,旨在为专业从业者提供深入的技术洞见和实
📒博客首页:崇尚学技术的科班人 🍣今天给大家带来的文章是《Dubbo快速上手 -- 带你了解Dubbo使用、原理》🍣 🍣希望各位小伙伴们能够耐心的读完这篇文章🍣 🙏博主也在学习阶段,如若发
一、写在前面 我们经常使用npm install ,但是你是否思考过它内部的原理是什么? 1、执行npm install 它背后帮助我们完成了什么操作? 2、我们会发现还有一个成为package-lo
Base64 Base64 是什么?是将字节流转换成可打印字符、将可打印字符转换为字节流的一种算法。Base64 使用 64 个可打印字符来表示转换后的数据。 准确的来说,Base64 不算
目录 协程定义 生成器和yield语义 Future类 IOLoop类 coroutine函数装饰器 总结 tornado中的
切片,这是一个在go语言中引入的新的理念。它有一些特征如下: 对数组抽象 数组长度不固定 可追加元素 切片容量可增大 容量大小成片增加 我们先把上面的理念整理在这
文章来源:https://sourl.cn/HpZHvy 引 言 本文主要论述的是“RPC 实现原理”,那么首先明确一个问题什么是 RPC 呢?RPC 是 Remote Procedure Call
源码地址(包含所有与springmvc相关的,静态文件路径设置,request请求入参接受,返回值处理converter设置等等): spring-framework/WebMvcConfigurat
请通过简单的java类向我展示一个依赖注入(inject)原理的小例子虽然我已经了解了spring,但是如果我需要用简单的java类术语来解释它,那么你能通过一个简单的例子向我展示一下吗?提前致谢。
1、背景 我们平常使用手机和电脑上网,需要访问公网上的网络资源,如逛淘宝和刷视频,那么手机和电脑是怎么知道去哪里去拿到这个网络资源来下载到本地的呢? 就比如我去食堂拿吃的,我需要
大家好,我是飞哥! 现在 iptables 这个工具的应用似乎是越来越广了。不仅仅是在传统的防火墙、NAT 等功能出现,在今天流行的的 Docker、Kubernets、Istio 项目中也经
本篇涉及到的所有接口在公开文档中均无,需要下载 GitHub 上的源码,自己创建私有类的文档。 npm run generateDocumentation -- --private yarn gene
我最近在很多代码中注意到人们将硬编码的配置(如端口号等)值放在类/方法的深处,使其难以找到,也无法配置。 这是否违反了 SOLID 原则?如果不是,我是否可以向我的团队成员引用另一个“原则”来说明为什
我是 C#、WPF 和 MVVM 模式的新手。很抱歉这篇很长的帖子,我试图设定我所有的理解点(或不理解点)。 在研究了很多关于 WPF 提供的命令机制和 MVVM 模式的文本之后,我在弄清楚如何使用这
可比较的 jQuery 函数 $.post("/example/handler", {foo: 1, bar: 2}); 将创建一个带有 post 参数 foo=1&bar=2 的请求。鉴于 $htt
如果Django不使用“延迟查询执行”原则,主要问题是什么? q = Entry.objects.filter(headline__startswith="What") q = q.filter(
我今天发现.NET框架在做计算时遵循BODMAS操作顺序。即计算按以下顺序进行: 括号 订单 部门 乘法 添加 减法 但是我四处搜索并找不到任何文档确认 .NET 绝对 遵循此原则,是否有此类文档?如
已结束。此问题不符合 Stack Overflow guidelines .它目前不接受答案。 我们不允许提出有关书籍、工具、软件库等方面的建议的问题。您可以编辑问题,以便用事实和引用来回答它。 关闭
API 回顾 在创建 Viewer 时可以直接指定 影像供给器(ImageryProvider),官方提供了一个非常简单的例子,即离屏例子(搜 offline): new Cesium.Viewer(
As it currently stands, this question is not a good fit for our Q&A format. We expect answers to be
我是一名优秀的程序员,十分优秀!