AQS3---出队(凌乱,供参考,先看AQS2)

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

 

出队: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=headprev=前面staus正常的节点)。如果第一个status正常的节点=第一个真正正常的节点,那么第一个真正正常节点会去自旋获取锁(前提是没有阻塞)并且和head唤醒的也是同一个。

 

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

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

 

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


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

 

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

 如果head=C=0,准备出队,就不会去唤醒head.next=Dhead线程直接返回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就不会去唤醒等待的第一个节点
    }
原文地址:https://www.cnblogs.com/yaowen/p/11320687.html