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

出队: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就不会去唤醒等待的第一个节点
}

最新文章

  1. MySQL索引原理及慢查询优化
  2. 最近在新公司的一些HTML学习
  3. [ASP.NET Core] Getting Started
  4. [lua]安卓ndk如何编译lua库
  5. UWP的拖拽功能
  6. 孙鑫MFC学习笔记16:异步套接字
  7. Asp.net WebApi + EF 单元测试架构 DbContext一站到底
  8. 百度之星Astar2016 Round2A
  9. Delphi如何让程序最小化到任务栏(截取WM_SYSCOMMAND后,调用Shell_NotifyIcon)
  10. css3 2D变换 transform
  11. macOS apache配置及开启虚拟服务器的开启,apache开启重写模式
  12. C语言总结报告
  13. python笔记:#007#变量
  14. 伪指令 ADR 与 LDR 的区别
  15. Confluence 6 MySQL 3.x 字符集编码问题
  16. 译:SOS_SCHEDULER_YIELD类型等待在虚拟机环境中的增多
  17. vue框架(三)_vue引入jquery、bootstrap
  18. tomcat点击startup.bat一闪而退的方法
  19. Asp.Net Core 视图整理(一)
  20. 【HTML】如何判断当前浏览器是否是IE

热门文章

  1. 使用Alipay代码源,构建自己的Docker镜像
  2. Service Mesh服务网格新生代--Istio
  3. Qt keyevent学习笔记
  4. 使用PSCI机制的SMP启动分析
  5. 第一部分day5 文件操作
  6. 大数据技术原理与应用【第五讲】NoSQL数据库:5.5 从NoSQL到NewSQL数据库
  7. Java精通并发-Condition详解及相比于传统线程并发模式的改进
  8. rtmpdump应用在window中
  9. 02-C#笔记-类的定义
  10. 达信:深度解读COSO新版企业风险管理框架(ERM)