zoukankan      html  css  js  c++  java
  • 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就不会去唤醒等待的第一个节点
        }
  • 相关阅读:
    storm原理写得比较好的文章
    maven设置jdk版本
    项目中记录log4j记录日志
    eclipse jadeclipse配置
    Maven使用说明
    crond不执行原因分析
    空调遥控器图标含义
    window7开放端sqlserver端口
    servlet仿struts参数注入
    cocos 2dx-js3.5整合anySDK
  • 原文地址:https://www.cnblogs.com/yaowen/p/11320687.html
Copyright © 2011-2022 走看看