发令枪CountDownLatch底层原理解析(看完肯定懂系列)

    CountDownLatch是JUC包下比较有名的并发工具,也就是大家熟知的发令枪,类似Lock,借助了AQS队列同步器来完成功能,下面是比较简单的例子:

 CountDownLatch latch=new CountDownLatch(2);
        Work worker1=new Work("程序员1", 5000, latch);
        Work worker2=new Work("程序员2", 8000, latch);
        worker1.start();//
        worker2.start();//
        latch.await();//
        System.out.println("程序员可以下班了,下班时间: "+sdf.format(new Date()));

Work是线程定义如下:

class Work extends Thread{
        String Name;
        int workTime;
        CountDownLatch latch;
        public Work(String Name ,int workTime ,CountDownLatch latch){
            this.Name=Name;
            this.workTime=workTime;
            this.latch=latch;
        }
        public void run(){
            System.out.println(Name+" 开始工作,现在时间是 "+sdf.format(new Date()));
            doSomething();
            System.out.println(Name+" 工作完成,现在时间是 "+sdf.format(new Date()));
            latch.countDown();

        }
        private void doSomething(){
            try {
                Thread.sleep(workTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

输出结果如下:

程序员1 开始工作,现在时间是 2018-05-03 16:18:45
程序员2 开始工作,现在时间是 2018-05-03 16:18:45
程序员1 工作完成,现在时间是 2018-05-03 16:18:50
程序员2 工作完成,现在时间是 2018-05-03 16:18:53
程序员可以下班了,下班时间: 2018-05-03 16:18:53

从结果可以看出两个程序员一起开始工作(时间不一定完全一致),两者都工作完成后latch.await()才通过。由此可以看出发令枪的作用是让所有的线程等着所有其他线程直到所有的线程都完成工作后放行,有点像考四六级时候,保安堵着大家直到考官把所有试卷收完才放我们出去微笑,也有点类似跑步比赛里面,要等所有参赛者就位后才鸣枪(发令枪的由来)。因此对于拓扑结构的工作用发令枪应该再合适不过了。

        由上面的简单例子可以看到,发令枪的主要功能实现是由latch.countDown()和latch.await()实现的。接下来是源码的追踪环节。

        在程序的最开始由一句new的过程,这个是构造同步器的state的初值

this.sync = new Sync(count);

countDown方法执行了如下操作:

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { //尝试用CAS的方式给做state--的操作,具体实现代码在CountDwonLatch类的内部类Sync中
            doReleaseShared();//只有最后完成任务的线程执行
            return true;
        }
        return false;
    }

在state不等于1的时候其他线程会在下面的方法里返回false直到state==1&&nextc ==0:

 protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

我们重点看一下doReleaseShared()这里做了什么(最后一个线程执行一次):

 for (;;) {    //自旋操作
            Node h = head;    //h指向同步器的头结点
            if (h != null && h != tail) { //队列同步器不为空且至少存在两个非空节点
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))//SIGNAL状态表示当前节点正在等待被唤醒
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);  //给该节点的后续节点上的线程通行,相当于唤醒主线程
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//,将h的等待状态设置为PROPAGATE
                    continue;                // loop on failed CAS
            }
            if (h == head)                   
                break;
        }

需要注意的是,head和tail节点是辅助节点,并不包含任意线程信息。对于CountDownLatch来说,最终只需要把主线程释放执行就完成了功能,而执行任务的线程并没有进入到队列中。这里解释一下等待状态,等待状态在队列同步器里面可以说是管理策略的实际执行单元,它里面包含了5个状态值,代表了不同的作用:

    1. 初始化状态,值=0       

    2.SIGNAL 值=-1,实际意义是如果当前结点释放了同步状态后会去唤醒后续节点

    3.PROPAGATE 值=-3 实际意义是将后续的节点会一直以共享式获取同步状态

    4.CONDITION 值=-2 这个是比较特殊的状态,它的出现意味着它离开了同步队列进入了Condition对象的等待队列,通常是在同步队列的节点执行了await方法

    5.CANCELED 值=-1 在同步队列里的节点碰到了等待超时或者被中断的状态,只要进了该状态相当于该节点上的线程死了

总结起来就是所有节点都以共享的状态去对state做修改。

接下来看一下await方法,我使用上面的小例子,使用jconsole追踪它的堆栈信息如下:



从图里能很清楚的看到await方法阻塞的具体位置是parkAndCheckInterrupt(),回到await的源码:

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;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

一开始我有想过,如果给我去设计类似的功能应该怎么弄,我的想法是把所有的线程都进入到Condition的等待队列,然后统一唤醒他们,但是这个唤醒的机制必须是最后一个完成的线程来做,在运行过程中没办法确定最后完成的是哪个线程,因此这个并发工具实际上就是确定最后一个完成的线程然后结束其他线程。

直观上看实在无法理解里面的流程,我准备用debug的方式一步一步地跟踪最后await是如何使得主线程直接退出的。


首先是await先执行addWaiter方法:

未执行之前,状态如下:


此时同步队列为空,根据该方法的定义会进入enq(node)方法:


    根据之前的状态可以知道tail本来就是null,因此该方法第一次执行完会使得head和tail指向同一个节点且不为null;然后第二次会将传入的node加到原先的tail后面成为新的tail然后返回,这时候同步队列里有两个节点,一个是head,一个是node,此时node里包含了主线程main。

    然后又回到doAcquireSharedInterruptibly(),因为p==head是始终成立,所以会一直查询state==0是否成立,如果成立,在这个例子里是又去执行了一遍doReleaseShared(),这里应该是考虑到执行顺序,因为countDown方法同样会执行该方法,而该方法是用来给主线程解锁的,如果state==0之后,await先探测到这个状态可以提前结束.

    还有一个函数shouldParkAfterFailedAcquire(p, node),因为p始终是node的前继节点head,node始终是tail,所以执行完下面这句话就直接返回false:

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
如果在自旋中再次执行上面的函数会抛出异常,但是这个异常会被处理掉,这样就保证了head的等待状态一直是SIGNAL。

上述功能其实用Thread.join也能实现,但是会降低cpu的使用效率。

应用场景:比赛里进行排名计算,或者是大数据文本的解析过程





        

猜你喜欢

转载自blog.csdn.net/qq_36243399/article/details/80181748