• Java CountDownLatch解析(下)


    • 写在前面的话

    上一篇CountDownLatch解析中,我们了解了CountDownLatch的简介、CountDownLatch实用场景、CountDownLatch实现原理中的await()方法,

    接下来我们接着来了解它的countDown()方法以及它的示例和优缺点。

    • CountDownLatch实现原理

    二、CountDownLatch.countDown() 

      关于同步队列那点事

    当部分线程调用await()方法后,它们在同步队列中被挂起,然后自省的检查自己能否满足醒来的条件(还记得那个条件吗?1、state为0,2、该节点为头节点),

    如果满足它将被移除头节点,并将下一个节点设置为头节点。具体如下图:

    这个Node是AbstractQueuedSynchronizer(下文简称AQS)中的静态内部类,其中它又有两个属性:

    volatile Node prev;
    
    volatile Node next;

    volatile的prev指向上一个node节点,volatile的next指向下一个node节点。当然如果是头节点,那么它的prev为null,同理尾节点的next为null。

    private transient volatile Node head;
    
    private transient volatile Node tail;

    head和tail是AQS中的属性,它们用来表示同步队列的头节点和尾节点。(有兴趣的同学可以引申看一下 transident 这个关于序列化关键字,这里不展开啦=_=)

    了解了同步队列后,接着我们开始线程调用countDown()方法后,到底发生了什么事?这儿咱们可以想象一下,可能计数值会减去1,而且还会判断state是不是等于0,

    如果不等于0,这个线程就继续往下走了;如果等于0,那么它可能还需要去叫醒这群挂起的线程。

    到底是不是这样呢,别方,跟着笔者一起继续来扒源码。

    public void countDown() {
       sync.releaseShared(1);
    }

    当调用CountDownLatch.countDown()后,它转而调用了Sync这个内部类实例的releaseShared()方法。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;     //退出该方法
        }
        return false;        //退出该方法
    }

    在Sync类中并没有releaseShared()方法,所以应该是继承与AQS,咱们看到AQS这个方法中,退出该方法的只有两条路。tryReleaseShared(arg)条件为真执行一个doReleaseShared()退出;条件为假直接退出。

    类比我们的猜测,这个条件很有可能就是state是否为0的判断。。。接着来看。

    protected boolean tryReleaseShared(int releases) {
        for (;;) {//死循环
            int c = getState();// 获取主存中的state值
            if (c == 0) //state已经为0 直接退出
                return false;
            int nextc = c-1; // 减一 准备cas更新该值
            if (compareAndSetState(c, nextc)) //cas更新
                return nextc == 0; //更新成功 判断是否为0 退出;更新失败则继续for循环,直到线程并发更新成功
        }
    }

    看到这儿四不四灵光从脑子喷涌而出啦。我们的猜测是正确的!

    private void doReleaseShared() {
        for (;;) {//又是一个死循环
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {//如果当前节点是SIGNAL意味着,它正在等待一个信号,或者说它在等待被唤醒,因此做两件事,1是重置waitStatus标志位,2是重置成功后,唤醒下一个节点。
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            
                    unparkSuccessor(h);
                }else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//如果本身头节点的waitStatus是出于重置状态(waitStatus==0)的,将其设置为“传播”状态。意味着需要将状态向后一个节点传播。
                    continue;                
            }
            if (h == head)                   
                break;
        }
    }

    同学们(敲黑板...),我们为啥要执行这个方法呀,因为state已经为0啦,我们该将同步队列中的线程状态设置为共享状态(Node.PROPAGATE,默认状态ws == 0),并向后传播,实现状态共享。

    这就是为啥方法名有个shared,因为这个共享状态需要传播下去,而不是一个节点(线程)独占。看看这个死循环,退出的路只有一条,那就是h==head,即该线程是头节点,且状态为共享状态。

     这里再啰嗦一句,可能读者会问,state已经等于0了,我们也通过循环的方式把头节点的状态设置为共享状态,但是它怎么醒过来的呢?既然这样,那我们再看一次上一篇中的代码

    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;// 计数值为0 并且为头节点 跳出循环
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();// 响应打断 跳出循环
            }
        } finally {
            if (failed)
                cancelAcquire(node);// 如果是打断退出的 则移除同步队列节点
        }
    }

    这个就是在同步队列中挂起的线程,它们自旋的形式查看自己是否满足条件醒来(state==0,且为头节点),如果成立将调用setHeadAndPropagate这个方法

     private void setHeadAndPropagate(Node node, int propagate) {
         Node h = head; // Record old head for check below
         setHead(node);
         if (propagate > 0 || h == null || h.waitStatus < 0 ||
             (h = head) == null || h.waitStatus < 0) {
             Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

    这个方法是将当前节点的下一个节点设置为头节点,且它也调用了doReleaseShared这个方法,我们刚才也说了,这个方法就是将头节点设置为共享状态的,由此,共享状态传播下去。

    至此,CountDownLatch实现原理中的countDown()方法剖析结束。

    • CountDownLatch示例

    废话不多说,直接上代码:

    package com.test.demo;
    
    import java.util.concurrent.CountDownLatch;
    
    /** 
    * @Title: TestCountDownLatch.java
    * @Describe:
    * @author: Mr.Yanphet 
    * @Email: mr_yanphet@163.com
    * @date: 2016年9月18日 上午11:22:42  
    * @version: 1.0 
    */
    public class TestCountDownLatch {
        
        private static final int taskNum = 5;
        
        static class MyRunnable implements Runnable {
            
            private int num;
            
            private CountDownLatch cdl;
            
            public MyRunnable(int num, CountDownLatch cdl){
                this.num = num;
                this.cdl = cdl;
            }
            
            public void run() {
                System.out.println("第" + num + "个线程开始执行任务...");
                try {
                    Thread.sleep(5 * 1000); // 模拟任务耗时
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("第" + num + "个线程任务执行结束...");
                cdl.countDown();
            }
        }
        
        public static void main(String[] args) {
            CountDownLatch cdl = new CountDownLatch(taskNum);
            for (int i = 1; i <= taskNum; i++) {
                MyRunnable mr = new MyRunnable(i, cdl);
                Thread t = new Thread(mr);
                t.start();
            }
            System.out.println("等待其他线程完成任务才继续执行...");
            try {
                cdl.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("其他线程完成任务,主线程开始执行...");
            System.out.println("主线程任务完成,整个任务进度完成...");
        }
    
    }

    执行结果如下:

    第4个线程开始执行任务...
    第5个线程开始执行任务...
    第3个线程开始执行任务...
    第2个线程开始执行任务...
    第1个线程开始执行任务...
    等待其他线程完成任务才继续执行...
    第4个线程任务执行结束...
    第2个线程任务执行结束...
    第1个线程任务执行结束...
    第3个线程任务执行结束...
    第5个线程任务执行结束...
    其他线程完成任务,主线程开始执行...
    主线程任务完成,整个任务进度完成...

    主线程等待5个子线程执行完任务,才继续往下执行自己的任务。

    • CountDownLatch的优缺点

    优点:

    CountDownLatch的优点毋庸置疑,对使用者而言,你只需要传入一个int型变量控制任务数量即可,至于同步队列的出队入队维护,state变量值的维护对使用者都是透明的,使用方便。

    缺点:

    CountDownLatch设置了state后就不能更改,也不能循环使用。

    以上就是关于CountDownLatch学习的全部内容。因为笔者也是菜鸟,所以站在菜鸟的角度分析源码,难免重复啰嗦。如有任何问题,希望大家指正。谢谢~~~

  • 相关阅读:
    bodybuilding
    DBLINK的session无法关闭,报异常!
    失控
    eclipse
    Linux下查看用户列表
    org.apache.commons.httpclient
    java map 遍历
    java String split
    胸上肌到底要怎么练啊!
    POI操作Excel常用方法总结
  • 原文地址:https://www.cnblogs.com/yanphet/p/5852244.html
Copyright © 2020-2023  润新知