出队时候,如果队列处于稳定状态,那么就是一个挨着一个出队,难点在于出队的时候,队列正处于调整阶段,那么此时队列中的关系是混乱无章可寻的。
出队:unlock释放锁,不在队列线程去抢锁,队列第一个正常节点没有阻塞会自旋时候尝试获取锁,head=-1,唤醒第一个status正常节点(可能是head.next,也可能是从tail到左找到的第一个status正常的节点)。
所以前面讲的为什么都要把前面正常节点置为-1后再去阻塞(设置-1失败再去往前设置),异常节点(后面正常节点阻塞在这个异常节点上)也要把前面正常节点置为-1再退出(如果前面正常节点=head,但是head=0已经出队失败了,设置-1也会卡死,此时要唤醒后面节点)(如果设置-1失败:异常前变成了头节点,并且出队失败了,再异常,再设置=-1失败,head=1了,就要唤醒后面,参见下图),就是为了即使栅栏区间的节点都停止活跃了,通过前面正常节点能够把后面正常节点出队。
反例就是:出队的节点=0,那么就不能够唤醒后面的节点,此时也要保证后面正常出队(如果没有外部线程再去调用unlock)。
- 正常节点没有阻塞,出队正常
- 正常节点阻塞,异常节点去唤醒正常节点
如果unlock释放锁时候,head=0,就不会去唤醒head后面status正常的节点。如果head后面第一个真正正常节点阻塞了(只有在队列的第一个正常节点才会自旋时候尝试获取锁),那么就永远不能出队。所以head=0通过head唤醒出队失败了,head后面的节点要能够自行出队(自行出队也只是第一个真正正常节点会去主动尝试获取锁,不是第一个真正正常节点不会主动尝试获取锁)。这时候:
- head=0,后面第一个真正正常节点还在自旋没有阻塞,看到自己是第一个节点会去尝试获取锁(不会导致不能出队)
- 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 < 0)//-1就设置为0,异常节点这里是1。 头结点要么是0要么是-1。头结点设置为0. compareAndSetWaitStatus(node, ws, 0); /*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 > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) //右边的正常节点。 if (t.waitStatus <= 0)// 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 > 0)//找到新的-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 <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL) ) ) && pred.thread != null //再次判断不是头节点 ) { //pred不是头节点,并且,pred正常 //有可能这时候prev=head,但是head=-1,可以正常唤醒。 Node next = node.next; if (next != null && next.waitStatus <= 0)//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 > 0) {//waitStatus = 1,清理CACELLED节点。 do { node.prev = pred = pred.prev;//pred往前进一个, } while (pred.waitStatus > 0);//<=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 != 0) 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就不会去唤醒等待的第一个节点 }