JUC--006--locks2

接着上篇
调用 countDownLatch.await(); 的线程,创建了 Node, 并添加到 双向链表中。
而且将自己的前置节点的状态设置成 SINGAL 状态,接着自己就阻塞在 unsafe.park 上。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
countDownLatch.countDown(); 的执行:

    //CountDownLatch::countDown
    public void countDown() {
        sync.releaseShared(1);
    }
    
    //AbstractQueuedSynchronizer::releaseShared
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
    
    //Sync::tryReleaseShared  
    protected boolean tryReleaseShared(int releases) {
        // 倒计时,数量减1, 减到0时返回true
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;   //已经为0了,为什么还在调用,不理他,返回false
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0; //正好减到0,返回 true
        }
    }
    
    //AbstractQueuedSynchronizer::doReleaseShared
    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            //head 为 null 或者 head 和 tail 相同,说明当前已经没有线程阻塞(没有Node)
            if (h != null && h != tail) {
                //能够进入,说明,head 后面最少有一个 Node
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    //而且 head 的状态时 SINGAL,  把 head 的状态改成 默认的0, 不成功就继续循环
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    //把 head 的状态从 SINGAL 改成 0.  head 有义务唤醒它的下一个节点
                    //因为前面有 continue; 所以执行到这里必然是把 head 的状态设置成 0 了。
                    
                    //这个方法作用是解除参数 h 下一个节点的阻塞,具体代码在下面
                    unparkSuccessor(h);
                }
                //ws 等于 0, 说明 head 的状态没有设置成功 SIGNAL, 为什么会这样呢?
                //通过前面的代码看,如果node添加到链表,那么在阻塞前一定会把前一个节点的状态设置成 SIGNAL
                //为什么 这里 head 的状态还是0。 可能这里的代码 CountDownLatch 用不到。先不关心把
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            
            //前面 h 是 head 的赋值, 这里还用判断吗,肯定相等啊
            //不一定, head 是成员变量,会不会有线程被唤醒,在其他方法中修改了 head 的值??
            //还真的会,经过上面的代码,可能已经唤醒过线程,被唤醒的线程是可能修改 head 的,后面会看到
            if (h == head)                   // loop if head changed
                break;
        }
    }
    
    //这个方法是让参数 node 的下一个节点解除阻塞。
    //AbstractQueuedSynchronizer::unparkSuccessor
    private void unparkSuccessor(Node node) {
        /*
         * 参数是上面传递的 head,  状态已经从 SINGAL 改成 0
         */
        int ws = node.waitStatus;
        //从上面的调用,可知, ws 一定是0, 
        //这个方法不止一个调用者, 所以这个判断应该是其他调用者使用的。 本处调用用不到
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        //取出下一个,不为空
        if (s == null || s.waitStatus > 0) {
            //下一个节点为空,或者下一个节点状态大于0(大于0 是被取消的状态)
            // head--x---A---B---C---D(tail)   x 是不可用节点了,即状态 > 0
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                //那就从后向前找,找到离head最近的,而且状态还 <= 0的节点,赋值给 s
                if (t.waitStatus <= 0)
                    s = t;
        }
        
        //执行这里而且 s 不为空,两个情况,
        //s 就是参数 node 的后一个节点,因为它微托前一个唤醒它。此时 不考虑s自己的状态
        //s 不是 node 的后一个节点。 
        //不管哪种情况,s不是 null, 就会被唤醒。
        if (s != null)
            LockSupport.unpark(s.thread);   //UNSAFE.unpark(thread);
    }
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//AbstractQueuedSynchronizer::doAcquireSharedInterruptibly
//这个方法前面说过, 添加节点,把添加的节点的前一个节点的状态设置成 SINGAL
//并且阻塞在 ----------------A 上
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();            //----------------B  
            if (p == head) {
                int r = tryAcquireShared(arg);            //----------------C
                if (r >= 0) {
                    setHeadAndPropagate(node, r);         //----------------D
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())                  //----------------A
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

经过上面的代码分析,倒计时已经是 0 。而且从 head 节点开始,head 负责唤醒它的下一个节点
所以就有线程 从 ----------------A 中返回了, 因为唤醒线程是 unsafe.unpark, 而不是
thread.interrupte ,所以返回false, 不会抛出异常。 接着又是一个循环。

再重复一次:唤醒是从 head 开始的, head 负责唤醒自己的下一个。
就是上面的代码 unparkSuccessor(head) 唤醒的 head 的下一个。 

----------------B:
head 后的 node 代表的线程 从 ---A 中唤醒以后,正常情况下,唤醒线程 node 的前一个就是 head
所以 ----B 处的 变量 p 就是 head, 能够进入 ----C

----------------C:
获取资源,因为当前倒计时已经是 0 了, 所以 CountdownLatch 复写这个方法返回了 1
当然了,如果倒计时还没有到 0时, 返回 -1, 接着的if判断代码就不会执行了。
此时这里的 r 是1, 能够进入 ----D

----------------D:
执行这里说明了,倒计时为0, 开始让阻塞的线程恢复运行了(而且head后的一个node已经解除阻塞)。
    //也就是说,这时是第一个唤醒的线程在运行。
    //参数 node 就是 head 的下一个节点 
    //而且第二个参数大于0, 就是 countDownlatch 返回的 1
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head;  // h 持有 head
        setHead(node);  // 然后就把这个节点设置成 head 了。(它把head干掉,自己当head)
    
        //这里 propagate = 1, 直接进入 if, 至于后面还有那么多情况,不知道怎么触发
        //还有其他很多情况,比如带 timeout 的阻塞,propagate 就还是小于0的情况
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //能够进来, node 是新的 head, 取下一个
            Node s = node.next;
            
            //s.isShared() 里面只有一行代码: return nextWaiter == SHARED;
            //看一下 node的创建: Node node = addWaiter(Node.SHARED);
            //所以s不为空就能进来。
            if (s == null || s.isShared())  
                //这个方法在倒计时为0时已经调用了,在这里又调用,接着唤醒
                //head 的下一个(head 已经不是原来的head 了)
                doReleaseShared();
        }
    }
代码执行 ------D, 是因为一个线程被唤醒, 然后把 唤醒的node 设置成一个新的 head, 
接着唤醒下一个线程。 ------D 执行完后(---D在循环调用,每次循环,换一个head, 然后唤醒
head 的下一个,前一个只负责唤醒自己的下一个,直到链表用完), 一个线程被唤醒,
开始一步步按照调用堆栈返回到 用户代码中 countDownLatch.await(); 这里。 
这样,用户代码阻塞在 countDownLatch.await(); 上的就能接着执行了。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
虽然还有很多无法理解的代码,但还是走完了一个流程。小小的总结一下:
创建一个 CountDownLatch 对象, 设置有 N 个资源。
然后有 M 个线程需要等待着 CountDownLatch 倒计时为 0 , 所以这 M 个线程依次的
创建属于自己的 Node 对象,添加到 一个头为 head 的 双向链表中。而且把自己的前一个
节点的状态设置为 Singal, 意味着前一个节点有义务唤醒自己。这样设置完成以后。
这个线程就进入无限期的阻塞状态(unsafe.park完成阻塞功能)。
于是便形成了这样的链表:head---M1----M2----...----Mm(tail)。 

然后还有一组线程,个数是 N, 当他们执行完自己的任务时,倒计时就减1. 直到第N个
线程执行完成然后把倒计时减1,此时倒计时已经为0。也就是说,让倒计时为0的哪个线程
要做一件大事——解救阻塞的 M 个线程。 于是,这个线程 找到 head,  解救了 M1。
然后 M1 被唤醒(unsafe.unpark), M1 将 head 干掉, 自己当上了 head 。 然后解救
head 的下一个, 也就是 M2。 M2 效仿 M1, 干掉 Head, 自己做 head。 然后解救
head 的下一个, 即M3。 ......
================================================================================
线程被 countDownLatch.await(); 阻塞
除了倒计时变成0, 还能怎样从阻塞中返回呢?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
看这个demo:
开启三个线程 + 主线程,
1线程,2号线程 主线程,先后阻塞  countDownLatch.await();
线程3号,在5秒以后 执行 thread1.interrupt(); 打断线程1。让线程1从阻塞中恢复

private static void t7() throws InterruptedException
{
    CountDownLatch countDownLatch = new CountDownLatch(1);
    Thread thread1 = new Thread(()-> {
        try{
            sleep(1000);
            countDownLatch.await();
        }catch (Exception e) {
            System.out.println("线程1异常:" + e.getClass());
        }
        System.out.println("线程1结束:" + Thread.currentThread().isInterrupted());
    }, "1号线程");
    Thread thread2 = new Thread(()-> {
        try{
            sleep(2000);
            countDownLatch.await();
        }catch (Exception e) {
            System.out.println("线程2异常:" + e.getClass());
        }
        System.out.println("线程2结束" + Thread.currentThread().isInterrupted());
    }, "2号线程");

    thread1.start();
    thread2.start();

    new Thread(()-> {
        sleep(5000);
        thread1.interrupt();
        System.out.println("线程3结束");
    }, "3号线程").start();

    sleep(3000);
    countDownLatch.await();
}

结果输出(符合期望):
线程3结束
线程1异常:class java.lang.InterruptedException
线程1结束:false

然后就是线程2和主线程永久阻塞了。
对应这个不太常规化的唤醒方式,源码又是在干什么呢?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false; //------------------C
                    return;
                }
            }
            //------------------A
            //现在的链表是: head--node1Thread--node2Thread--nodeMainThread
            //而且三个线程全部阻塞在 parkAndCheckInterrupt() 中。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);  //------------------B
    }
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
------------------A:
此时 线程3 让 线程1 interrupted。
线程1 从 parkAndCheckInterrupt() 中解除阻塞,并 return Thread.interrupted();
因为当前线程确实被interrupted, 所以返回 true。 ---A 处的 if 判断就满足了,
结果抛出异常。而且这个异常并没有在这里捕获。 所以会继续向外抛出。
然后就是 ----B 的执行,failed 的默认值是 true, 只会在 ----C 改成 false 。  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
------------------B:
//参数就是那个被打断的线程A所在的 Node.
//目前的链表: head--node1Thread--node2Thread--nodeMainThread(tail)
//而且除了最后一个状态是默认值0以外, 前面三个的状态都是 SINGAL,
//所以前三个都有义务要唤醒下一个节点

private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;

    //向前一直找到一个状态小于等于0的。
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    /* 
     * 如果能够上面进入了 while 循环
     * head---a--b---x---me--z(tail)   假设x是无效的(状态大于0), me 就是参数 node
     * 经过上面的 while 循环, prev 是 b, 而不是 x。 
     * 链表则变成  head<--->a<-->b<---me<-->z(tail)
     *                            \x 
     * b的后节点还是执向 x, 而不是指向 me, 而me的前节点执行 b
     * 如果现在 从head出发,是找不到 me 的。 但是从 z 出发可以找到 head
     */
    
    // 有效的前一个的后一个。 
    // predNext 有可能是 me 还可能是 x。 但是 prev 一定是 b
    Node predNext = pred.next;

    //当前 Node 设置成取消状态(node是me, 是被打断唤醒的节点)
    node.waitStatus = Node.CANCELLED;

    // 如果自己就是最后一个,也不用通知后面的节点了,
    // 如果自己是最后一个,&& compareAndSetTail(node, pred) 也没有竞争。
    // 因为每个线程都有自己的 node 。自己是 tail 。只能有一个线程在执行 && 后面的
    // 如果有多个线程(多个node,只有一个是 tail),那么有一个线程必然在 && 前面就被排除。
    if (node == tail && compareAndSetTail(node, pred)) {
        //把自己的前一个设置成tail, 把自己的前一个的后一个设置成 null.
        //这样相当于把自己从链表中删除。
        //如果 me 是最后一个,那就把 b 设置成新的 tail, 并且把 b 的后一个设置成 null, 
        compareAndSetNext(pred, predNext, null);
        //这里执行完成以后,一路返回到用户代码,从阻塞中返回,抛出前面那个未捕获的打断异常
    } else {
        /*
         * 来到这里,说明自己不是 tail
         * 那就看看自己的前一个是不是 head:
         * if 的成功了表明:
         *     自己的前一个不是 head, 
         *     而且 自己的前节点状态是 SINGAL 或者可以改成 SINGAL
         *     而且前一个的线程不为空
         */
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            //进入if了, 获取自己后面一个,如果后一个存在,而且状态是正常的。
            //那就把自己的后一个设置成自己前一个的后一个。(也就是  b-->z)
            //自己的前一个是 b, b的后一个是 自己或者是那个无效的 x
            //不管b的后面是谁,都要改成自己的后一个。 也就是 把 z 设置成 b 的后一个
            //
            //但是 z 的前一个并没有改变,还是 me, 应该也要把 z 的前一个设置成功 b 才好。
            //否则 z 持有 me 的引用,导致 me 无法释放。这应该是一个小bug吧
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            //当前节点前置节正是head, 
            //或者 当前节点的前节点不是 head, 但是他的状态不是 SINGAL, 或者无法改成 SIGNAL
            //或者 前节点不是 head, 状态能改成 SIGNAL, 但是线程为空。
            //这时要取唤醒本节点的下一个节点。 这里曾经让我很困惑。
            unparkSuccessor(node);
        }

        //这样帮助GC, 为什么不设置为 null 呢? 而且 node.prev 仍然是 b, b 还有效,也无法GC 吧
        node.next = node; // help GC
        
        //这个方法有点怪异, head 后面的节点应该都是一样的, 但是 head 后面的节点 a  (b, me)  z
        //他们的仅仅是位置不同,却进入了三个不同的分支。 为什么要这样做。
    }
}

private void unparkSuccessor(Node node) {
    //当前节点(被打断唤醒节点)的状态已经改成1了, 
    //前面倒计时正常结束时也看过这里的代码,这里是 -1.
    //这种使用 打断方式 结束阻塞的 现在状态时 1.
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 因为本节点已经被打断唤醒了, 本节点有义务唤醒下一个节点
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        //来到这里说明,自己的下一个节点,不正常, 要么为空,要么状态大于0
        s = null;
        //那就从tail 向前找, 直到离本节点最近的一个正常的节点。赋值给 s
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //如果找到了自己的后续节点,那就唤醒它。 这里让我更困惑,下面就想明白了
    if (s != null)
        LockSupport.unpark(s.thread);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
根据上面的例子分析: 在打断线程1之前,链表是这样的:
head--node1Thread--node2Thread--nodeMainThread
node1Thread 的前一个是 head, 所以直接就执行了 unparkSuccessor(xx)
结果就是线程2被唤醒。但是用户代码线程2并没有执行输出语句啊。
我就困惑在这里,为什么把线程2唤醒,但是线程2用户层面的代码没有执行。
我想:要么 LockSupport.unpark(s.thread); 执行,但是没有效果,要么线程2再次阻塞了。

其实是第二个:再次阻塞了。
这里让线程2从阻塞中返回。这个解除线程2阻塞的方式 unpark, 而不是线程中断。
所以,之前三个线程都阻塞的地方(前面的 ---------A 处代码), 线程2返回了,
因为不是打断,不会进入if,不会抛异常了。 所以再次循环了。结果,又来到老地方阻塞了。 

所以这也是 SIGNAL 状态的意思: 有义务唤醒自己的下一个节点。
自己是正常唤醒 还是非正常唤醒, 都要唤醒自己的下一个。自己是非正常唤醒,然后自己
会正常唤醒下一个线程,下一个线程发现状态不符合,还会再次阻塞的。 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果 CountDownLatch 的倒计时已经为 0了, 那么再次调用 countDownLatch.await();
不会再次阻塞,而是立刻返回。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值