出队时候,如果队列处于稳定状态,那么就是一个挨着一个出队,难点在于出队的时候,队列正处于调整阶段,那么此时队列中的关系是混乱无章可寻的。 

出队:unlock释放锁,不在队列线程去抢锁,队列第一个正常节点没有阻塞会自旋时候尝试获取锁,head=-1,唤醒第一个status正常节点(可能是head.next,也可能是从tail到左找到的第一个status正常的节点)。


所以前面讲的为什么都要把前面正常节点置为-1后再去阻塞(设置-1失败再去往前设置),异常节点(后面正常节点阻塞在这个异常节点上)也要把前面正常节点置为-1再退出(如果前面正常节点=head,但是head=0已经出队失败了,设置-1也会卡死,此时要唤醒后面节点)(如果设置-1失败:异常前变成了头节点,并且出队失败了,再异常,再设置=-1失败,head=1了,就要唤醒后面,参见下图),就是为了即使栅栏区间的节点都停止活跃了,通过前面正常节点能够把后面正常节点出队。

反例就是:出队的节点=0,那么就不能够唤醒后面的节点,此时也要保证后面正常出队(如果没有外部线程再去调用unlock)。

  1. 正常节点没有阻塞,出队正常
  2. 正常节点阻塞,异常节点去唤醒正常节点

如果unlock释放锁时候,head=0,就不会去唤醒head后面status正常的节点。如果head后面第一个真正正常节点阻塞了(只有在队列的第一个正常节点才会自旋时候尝试获取锁),那么就永远不能出队。所以head=0通过head唤醒出队失败了,head后面的节点要能够自行出队(自行出队也只是第一个真正正常节点会去主动尝试获取锁,不是第一个真正正常节点不会主动尝试获取锁)。这时候:

  1. head=0,后面第一个真正正常节点还在自旋没有阻塞,看到自己是第一个节点会去尝试获取锁(不会导致不能出队)
  2. Head=0,第一个真正正常节点阻塞,那么他肯定设置过前面某个节点=-1,并且不是head,说明中间至少有一个异常节点,异常节点退出时候要唤醒第一个真正正常节点,否则就会卡死。

第一个真正正常节点如果!=第一个status正常节点那么第一个真正正常的节点不会自旋获取锁(因为第一个真正正常节点prev!=head,prev=前面staus正常的节点)。如果第一个status正常的节点=第一个真正正常的节点,那么第一个真正正常节点会去自旋获取锁(前提是没有阻塞)并且和head唤醒的也是同一个。

稳定状态下:正常节点阻塞前要保证前面有一个-1,不然出队时候就唤醒不了自己,那么整个队列就卡死在这里了。如果这个认定的-1异常了,那么这个异常节点退出前就要保证前面有-1,如果不能保证就要唤醒后面。

不稳定状态下:在某一时刻,正常节点前面没有-1也可以,但是在一个栅栏范围内,必须有节点还活跃。

unlock时候要能够保证第一个正常节点能够出队。依次类推。


一个节点=0,说明在后面一个栅栏区间内,至少还有一个线程处于活跃状态,2个异常节点会把他置为-1再退出,一个正常节点可能会把他置位-1在阻塞,所以节点=0最终都会置位-1的。但是节点=0时候正好要出队就会有问题:

但是如果此时head=C=0,准备出队,就不会去唤醒head.next=D,head线程直接返回true结束了。如果D没有阻塞会把C=-1,并且看到自己是第一个节点会去获取锁。即使D不主动去获取锁,如果还有其他没有入队的线程调用unlock时候发现head=C=-1也会去唤醒head.next=D。

如果head=C=0,准备出队,就不会去唤醒head.next=D,head线程直接返回true结束了,head不变仍然=C(只在获取锁的节点线程会改变head)。此时D阻塞了并且阻塞的时候吧B当做前驱了(B刚刚是正常的,D设置为-1后再异常)(如果D打算阻塞在C上面,D设置C=-1后再旋转一次发现自己是第一个节点会去获取锁),虽然C会被异常节点AB设置=-1,如果永远没有外部线程再去调用unlock,就永远不会再次通过head=C出队,那么D就永远不会出队,并且自己也阻塞了,队列就卡死在这里了。那么此时AB线程就要去唤醒D。

所以A发现A的前驱是头结点就要唤醒A的后面(不一定是next,会从tail往前找)。

所以B异常了,一定要把前面设置一个-1,然后前面再把前面设置一个-1,一直到head。

先调用unpark在调用park就不会阻塞。

//node可能是head节点可能是失效节点。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < )//-1就设置为0,异常节点这里是1。 头结点要么是0要么是-1。头结点设置为0.
compareAndSetWaitStatus(node, ws, ); /*head下一个节点,从头结点开始一个个的找后继,直到把队列找完。
next节点是正常的就找next,next不是正常的,就不能再找next,因为next.next有可能是自己。
就tail往前找,从tail往前找prev一定能够吧所有正常节点找到(还会找到不正常的节点)。
如果找到的这个节点status<=0,然后唤醒,但是这个节点异常了只是状态没有修改过来,
那么唤醒的这个假正常节点就不会去获取锁,而是帮着正常节点做 ,做完退出。
*/ Node s = node.next;//node=head,head.next不一定是初始节点,可能被改过指向曾经正常的一个节点,这个节点现在正常与否未知。
//下一个节点s被取消,node还没来得及重新设置新的正常后继节点。队列还没有处于稳定状态。
//第一个节点被取消,就从后往前找下一个状态正常节点。也相当于是head后面第一个正常节点。 if (s == null || s.waitStatus > ) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) //右边的正常节点。
if (t.waitStatus <= )// 0/-1都唤醒
s = t;
}
if (s != null)//s可能=head,head.thread=null就什么都不做。
LockSupport.unpark(s.thread);
}
//有可能是0变到1,也有可能是-1变到1,
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;//异常节点,thread先清空, /*失效节点:不断改变自己的前驱 在改变status 最后改变next*/ Node pred = node.prev;//node就是头结点,pred就是null,
while (pred.waitStatus > )//找到新的-1/0,跳过已经取消的节点。
node.prev = pred = pred.prev;//断开node.prev。node.prev只有节点自己线程修改不用cas,
//while出来时候pred=0/-1,while之后pred可能变为1。 0/-1节点在任何时候都有可能变为1,每次判断都只是瞬间的值。 /*pred = pred.prev;不是改变属性
node.prev = pred是改变属性*/ Node predNext = pred.next;//此时pred可能是0/-1节点也可能是1节点. /* 节点异常了只能通过status和thread看得出来。否则外界不知道异常了。别人也是通过status来看这个节点是不是异常了,所以有延时。 */ node.waitStatus = Node.CANCELLED;//强制覆盖,不用cas,以这个为主。 //修改队列。去掉的节点是尾节点就要修改,尾节点就不需要prev.next了。
if (node == tail && compareAndSetTail(node, pred)) {//死循环的cas会重来,一次cas失败了就放弃让别人去做,优先级低于直接赋值。
//修改tail为pred,修改失败:不是尾节点了,加进来了新的节点。
//pred.next!=predNext就是有节点加进来了,并且pred.next=新的节点,就不置位null。
compareAndSetNext(pred, predNext, null);//tail的next置位null,GC } else {//不是尾节点,或者是尾节点但是加进来了新的节点,
int ws;
if (pred != head //node前面是head,就不要去设置后继关系,而是唤醒,
&&
/* 再次判断pred的状态是正常=0/-1
有可能这时候prev=head,并且有可能head=0已经出队了,pred.thread = null就要唤醒node后面*/ /*prev不是头结点并且异常了,如果不管了,如果后面正常节点阻塞了,并且前面没有正常节点
那么head=0 unlock出队失败时候,队列里面的就全阻塞了,就永远不能出队。*/ ( (ws = pred.waitStatus) == Node.SIGNAL|| (
ws <= && compareAndSetWaitStatus(pred, ws, Node.SIGNAL) ) )
&& pred.thread != null //再次判断不是头节点
)
{
//pred不是头节点,并且,pred正常
//有可能这时候prev=head,但是head=-1,可以正常唤醒。
Node next = node.next;
if (next != null && next.waitStatus <= )//next节点也可能是取消了。
//pred.next没有被修改。next不可能为null,因为线程退出了节点可能还在,
compareAndSetNext(pred, predNext, next);//把node也跳过,失败不重来,因为别的正常-1节点也会修改,让正常节点。 } else {//node.prev=head,ndoe异常,
//node不一定是紧挨着head的节点,正常节点在node后面并且在第一个栅栏里面,
//如果这里不唤醒,只是依靠unlock唤醒,但是unlock在head=0时候是不会唤醒的什么都不做的,unlock在这种情况不负责唤醒线程获取锁,
//那么就该第一个栅栏里面的正常节点去获取锁,防止正常节点阻塞了,这里就要去唤醒,否则unlock出队失败就永远不能唤醒。 //或者,node不是第一个节点但是prev状态值=1了,或者,node不是第一个节点prev状态值=0/-1但是prev的thread=null了
unparkSuccessor(node);//唤醒node这个异常节点的后面节点
} node.next = node; // help GC。断开node.next
}
}
//前驱节点是-1就阻塞。前驱节点是0就是初始化设置为-1,并且清理失效的节点。直到找到-1为止。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//第一次进来,pred.waitStatus=默认值0,然后设置=-1,parkAndCheckInterrupt()失败会再次进来,直接返回true,表示应该阻塞。
//第一次进来不阻塞,第二次进来阻塞。
int ws = pred.waitStatus;//0 1 -1 -2 -3。 //前驱=-1,当前节点node应该阻塞。SIGNAL表示释放后需要唤醒后继节点。
if (ws == Node.SIGNAL)//这一行pred=-1,下一行pred可能会变为1,
return true; //其余返回false,当前节点不应该阻塞。
//清理失效的节点,并把新的前驱节点设置为-1。跳过现在是1的,并且是1之后不会变回-1。
if (ws > ) {//waitStatus = 1,清理CACELLED节点。
do {
node.prev = pred = pred.prev;//pred往前进一个,
} while (pred.waitStatus > );//<=0作为边界
pred.next = node;//回指肯定成功。 直接赋值覆盖cas赋值。 } else {//0或者-3 -2,是1不会变为-1。就是前驱后继关系。
//ws此时=0/-3/-2就说明没有取消就设置为-1,ws=1说明取消了,不能设置为-1。 /* 再次判断pred的状态为正常=0 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//设置新的前驱waitStatus=-1,有可能失败。被自己改为了1就以1为主,这边失败算了。
//有可能马上被设置为1,即使cas了也不保证cas下一行状态没改变,只是保证获取ws到改变期间没有变化,这一行之后变化了不管。
} /*找前驱节点:不断改变自己的前驱 在改变找到节点的next 最后修改找到节点的status*/ return false;
}
//返回true当前线程就阻塞(就没有获取锁),返回false就不阻塞(就获取了锁)。一次只能一个线程执行(exclusiveOwnerThread=的线程执行),其余阻塞
// 节点加进去之后,就死循环(死循环也是在最前面才获取锁,否则死循环或者阻塞),
final boolean acquireQueued(final Node node, int arg) throws InterruptedException {
boolean failed = true;
try {
boolean interrupted = false;
//旋转1次,旋转2次,旋转3次,或者多次阻塞,compareAndSetWaitStatus(pred, ws, Node.SIGNAL)有可能一直失败,多次在于这里。
for (;;) {
final Node p = node.predecessor();
//是最前面的节点(节点从前到后依次获取锁),并且获取锁成功,就不阻塞,这个节点退出链表,更新链表,Lock方法返回。 if (p == head && tryAcquire(arg)) {//否则死循环(自旋出队)。获取锁的线程只有一个,所以这里是加锁。
//setHead()加锁了不用CAS(只有修改成员变量cas否则都是线程的局部内部变量)。
//head=node,node.thread=null,node.prev=null。thread就是调用这个函数的线程设置为null(已经获取了锁就移除这个节点)。 //head指向正在运行的线程的节点,就是不在排队中的节点的线程。 如果不在队列的线程获取了锁,head不变继续指向。
//如果队列中的线程获取了锁,head就指向新的获取锁的线程的节点。
setHead(node);//node变成空节点并升级为头节点。 status=0/-1没变。
p.next = null; // help GC,node变为头结点,head节点gc,
if(Thread.currentThread().getName().equals("1号窗口") ) {
throw new InterruptedException();
}
failed = false;//不走cancelAcquire(node),
// if(Thread.currentThread().getName().equals("1号窗口") ) {
// throw new InterruptedException();
// }
return interrupted;//已经获取了锁,就不用中断了,lock方法返回void,不用阻塞了。先执行finally再return。
} //不是最前面的节点,或者 是最前面的节点但是获取锁失败。判断当前线程是否需要阻塞。 //shouldParkAfterFailedAcquire()判断当前线程是否需要阻塞 (通过前驱节点判断)
//parkAndCheckInterrupt()阻塞当前线程 ,阻塞失败继续死循环。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;//如果状态为true说明发生过中断,会补发一次中断,中断唤醒的线程应该去中断而不是继续执行,即调用interrupt()方法
}
} finally {//不异常也走这里然后return,
if (failed)//node成为了头节点异常,线程退出,head节点还在,
//抛出任何异常,则取消任务。这里不catch,外层函数要catch,否则异常一直在。
cancelAcquire(node);
}
}
public final void acquire(int arg) throws InterruptedException   {//tryAcquire会先去获取锁,获取成功返回true否则false。
//addWaiter()穿建一个独占的节点添加到尾节点去。
//如果获取失败,则向等待队列中添加一个独占模式的节点,并通过acquireQueued()阻塞的等待该节点被调用(即当前线程被唤醒)。
//如果是因为被中断而唤醒的,则复现中断信号。
//acquireQueued()检查node的前继节点是否是头节点。如果是,则尝试获取锁;如果不是,或是但获取所失败,都会尝试阻塞等待。
//addWaiter时候线程异常了,不会吧head置为1.
if (!tryAcquire(arg) &&
//返回是否中断,如果返回中断,则调用当前线程的interrupt()方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();//interrupted = true当前线程中断执行。重放acquireQueued里面的中断。
}
public final boolean release(int arg) {//只有一个线程访问
if (tryRelease(arg)) {//true就表示OwnerThread=null了state=0了,减少state和OwnerThread。
//tryRelease()返回true表示已完全释放,可唤醒所有阻塞线程;否则没有完全释放,不需要唤醒。这个线程要再次unlock才去唤醒队列的节点。
Node h = head; //头结点只能是0/-1不可能是1。头结点由第一个入队节点创建,第一个入队节点线程异常了也只会设置第一个节点=1,不会影响到头结点,不会设置到头结点=1。
//头结点看成一直是正常节点,
if (h != null && h.waitStatus != )
unparkSuccessor(h);//head.waitStatus=-1才进来,唤醒head的后继 //h=null,头结点都还没有建立,队列也还没有建立。
/*或者h!=null&&head.waitStatus==0,
最近的正常节点不一定是紧挨着的节点,最近的正常节点中间可能还有异常节点。
head.waitStatus变成-1是由head后面的最近正常节点设置的(不一定是紧挨着的节点),说明曾经有一个正常节点执行完了3步,这个正常节点是否还正常未知。
head.waitStatus==0说明head后面都异常了,或者后面第一个正常节点(不一定是紧挨着的节点)还没有自旋到这一步,还没被阻塞 。说明没有曾经一个正常节点完成了3步,把他设置成-1。
如果head后面的正常节点都阻塞了(仅仅只需要看最近的正常节点),必然会设置head=-1,并且是由最近正常节点设置的。
*/
//lock时候head.waitStatus==0不出对,说明都不做,head不改变。唤醒时候head不变,只有获取到锁之后,head变化。
return true;
}
return false;//unlock就不会去唤醒等待的第一个节点
}

AQS3---出队(凌乱,供参考,先看AQS2)的更多相关文章

  1. .NET平台下几种SOCKET模型的简要性能供参考

    转载自:http://www.cnblogs.com/asilas/archive/2006/01/05/311309.html .NET平台下几种SOCKET模型的简要性能供参考 这个内容在cnbl ...

  2. 【WPF】SnapsToDevicePixels与UseLayoutRounding二者到底有什么区别?供参考

    原文:[WPF]SnapsToDevicePixels与UseLayoutRounding二者到底有什么区别?供参考 MSDN上解释了一大堆,二者对比来看,并不能发现什么明显的区别,微软爸爸也不知道多 ...

  3. Unity上一页下一页切换功能实现源码(仅供参考)

    在做项目时我们有时需要实现切换上一页下一页图片,切换上一首下一首歌曲等等类似的功能.这里写了个简单的实现源码(仅供参考),要是有更好的方法欢迎提出来,共同进步~ 以切换上一页下一页图片为例: usin ...

  4. VSS的运用小内容(针对于vs2008版本)(小的问题都是,仅供参考--只针对于菜鸟级的)

    自己开始接触vss 的时候有些小的习惯没有很好的养成,下面的有关VSS内容都是简单的迁入迁出的问题,(仅供参考) 1.文件的迁入迁出:(.txt..xlsx..doc) a:文件的覆盖问题: 对于文件 ...

  5. normalizr实践使用(个人总结,仅供参考)

    # normalizr实践使用 原数据 (自编数据,本数据仅供参考) var aaaObj ={ "id" : "0000000000000000000000000000 ...

  6. 2019第十届蓝桥杯C++B组题解(赛后重写的,不确保答案正确性,仅供参考)

    先说一下这次的感受吧,我们考场比较乱,开始比赛了,还有的电脑有故障,(向这些人发出同情),第一次认真参加比赛,真正比赛的时候感觉没有那么正式,很乱,各种小问题,(例如博主就没找到题目在哪里,找到后又不 ...

  7. 如何有效防止DEDE织梦系统被挂木马安全设置(仅供参考)

    尊敬的客户,您好!     感谢广大客户对我司工作的信任和支持!      我司在最近的一个多月内陆续发现多起因 DedeCMS 安全漏洞造成网站被上传恶意脚本的事件,入侵者可利用恶意脚本对外发送大量 ...

  8. c++使用优先队列时自定义优先出队顺序(和sort)

    优先队列也是一种先进先出的数据结构,元素从队尾入队,从队头出队,但是优先队列相较一般队列多了一个判断优先级的功能,在当前队列中,优先级最高的元素将被第一个删除. 先看一下优先队列的定义 templat ...

  9. 迄今微软不同时期发布的SQL Server各版本之间的大致区别,供参考查阅

    通过在互联网上收集及微软官方网站等途径获取相关资料进行整理汇总出Microsoft SQL Server各个版本(SQL Server 2008 R2.SQL Server 2012.SQL Serv ...

随机推荐

  1. Newtonsoft.Json使用技巧

    本篇将为大家介绍Newtonsoft.Json的一些高级用法,可以修改很少的代码解决上述问题. 阅读目录 Newtonsoft.Json介绍 基本用法 高级用法 总结 回到顶部 Newtonsoft. ...

  2. 设计模式之(十三)外观模式(Facade)

    外观模式思想 历史上牛人中成功逆袭,实现人生辉煌的人很多,这群人最耀眼的无疑是明太祖朱元璋,从一个放牛讨饭的最低层小屌丝逆袭到人类权力顶峰开国皇帝,确实是我等膜拜的对象.在发不断的发展过程中,其实就在 ...

  3. UUID生成库libuuid和crossguid

    libuuid是一个开源的用于生成UUID(Universally Unique Identifier,通用唯一标识符)的库. 可从https://sourceforge.net/projects/l ...

  4. Spring常用注解之一

    Spring中的常用注解 @Component 把普通 pojo 实例化到 Spring 容器中,相当于配置文件中的 泛指各种组件,就是说当我们的类不属于各种归类的时候(不属于@Controller. ...

  5. 微信小程序-获取当前位置和城市名

    微信小程序-获取当前城市位置 1, 获取当前地理位置,首先要拿到用户的授权wx.openSetting: 2,微信的getLocation接口,获取当前用户的地理位置(微信返回的是经纬度,速度等参数) ...

  6. 使用Git Flow规范!

    Git Flow常用的分支 Production 分支 也就是我们经常使用的Master分支,这个分支最近发布到生产环境的代码,最近发布的Release, 这个分支只能从其他分支合并,不能在这个分支直 ...

  7. Linux文本编辑器Vim使用

    1. 插入 o 在光标下插入新行 a 在光标后插入 i         在光标前插入 O 在光标上一行插入新行 A 在光标行尾插入 I 在光标行首插入 2.光标定位 gg 到第一行行首 G 到最后一行 ...

  8. CentOS6.10下yum安装MySQL5.7

    MySQL官网的Yum仓库快速指南:https://dev.mysql.com/doc/mysql-yum-repo-quick-guide/en/ 检查是否安装有MySQL数据库 rpm -qa | ...

  9. java.util.ConcurrentModificationException异常;java.util.ConcurrentModificationException实战

    写代码遇到这个问题,很多博客文章都是在反复的强调理论,而没有对应的实例,所以这里从实例出发,后研究理论: 一.错误产生情况 1 .字符型 (1)添加 public static void main(S ...

  10. adb命令操作蓝牙

    打开和关闭蓝牙BT adb root adb shell svc bluetooth enable adb shell svc bluetooth disable UI层 查询:adb shell s ...