并发编程 — CountDownLatch 详解

一、概述

类 CountDownLatch 是一个同步功能的辅助类,使用效果是给定一个计数,当使用这个CountDownLatch类的线程判断计数不为0时,则呈wait状态,如果为0时则继续运行。实现等待与继续运行的效果分别需要使用await()和countDown()方法来进行。调用await()方法时判断计数是否为0,如果不为0则呈等待状态。其他线程可以调用countDown()方法将计数减1,当计数减到为0时,呈等待的线程继续运行。而方法getCount()就是获得当前的计数个数。

二、使用场景

1、某一线程在开始运行前等待n个线程执行完毕

比如实现一个机票比价场景,需要调用各个航空公司的机票价格,然后进行价格排序,就可以通过 CountDownLatch 来实现,代码如下所示:


public class CountDownLatchExample1 {

    public static void main(String[] args) {
        List<String> airs = Arrays.asList("东方航空", "南方航空", "成都航空", "北京航空");

        //定义一个CountDownLatch对象,指定数量为航空公司的个数
        CountDownLatch latch = new CountDownLatch(airs.size());

        List<Pair<String, Integer>> list = new ArrayList<>();

        //定义四个线程模拟调用 航空公司的外部接口
        Thread[] threads = new Thread[airs.size()];
        for(int i = 0; i < airs.size(); i++){
            threads[i] = new Thread(() -> {
                try {
                    // 模拟调用耗时
                    TimeUnit.SECONDS.sleep(current().nextInt(10));
                    //构建价格
                    Pair<String, Integer> pair = new Pair<>(Thread.currentThread().getName(), current().nextInt(100));
                    list.add(pair);
                    System.out.println(pair.toString());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    latch.countDown(); //执行完后 是计数器减1
                }
            }, airs.get(i));
        }
        //启动所有的线程
        Stream.of(threads).forEach(Thread::start);
        System.out.println("等待所有的线程执行完");
        try {
            latch.await(); // 在此等待
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        list.sort((o1, o2) -> {
            if(o1.getValue() > o2.getValue()){
               return 1;
            }else if (o1.getValue().equals(o2.getValue())){
                return 0;
            }else {
                return -1;
            }
        });

        System.out.println("========最终结果=======");
        list.forEach(System.out::println);
    }


}

 执行结果:

2、实现多个线程开始执行任务的最大并行性


public class CountDownLatchExample2 {
    //裁判类
    static class Referee{
        private CountDownLatch downLatch = new CountDownLatch(1);
        private CountDownLatch latch;

         Referee(int count){
            latch = new CountDownLatch(count);
        }

        // 各就位
         void prepare()  {
            try {
                System.out.printf("线程[%s]准备就绪 \n", Thread.currentThread().getName());
                latch.countDown(); //运动员就位
                downLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.printf("线程[%s]结束 \n", Thread.currentThread().getName());
        }
        //开始执行
        public void start(){
            try {
                latch.await(); //等待运动员就位
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("开始");
            downLatch.countDown();
        }
    }

    // 运动员类
    static class SportsMan extends  Thread{
        private Referee referee;

        SportsMan(Referee referee) {
            this.referee = referee;
        }

        @Override
        public void run() {
            referee.prepare(); // 运动员准备
        }
    }


    public static void main(String[] args) {
        //定义裁判指定10个运动员
        Referee referee = new Referee(10);
        //定义 10 个运动员
        SportsMan[] mans = new SportsMan[10];
        for (int i = 0; i < 10; i++){
            mans[i] = new SportsMan(referee);
            mans[i].setName("Thread-" + i);
        }
        Stream.of(mans).forEach(Thread::start);
        referee.start();
    }
}

执行结果:

 三、源码解析

1、类结构

2、Sync 一个静态内部类

//构造方法 CountDownLatch 的构造方法最终调用的是 Sync的构造。
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count); //初始化计数器
        }

        // 获取当前计数器
        int getCount() {
            return getState();
        }

        // 试图在共享模式下获取对象状态
        // 申请资源 如果 数量为 0,则不进行阻塞, 否则进入阻塞
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

       // 试图设置状态来反映共享模式下的一个释放
        // 减少计数器数量,直到数量为0,
        protected boolean tryReleaseShared(int releases) {

             // 死循环保证最终这个状态值能设置成功
            for (;;) {
                // 获取当前状态值,CountDownLatch中的锁存器计数
                int c = getState();
                // 如果状态为0,表示CountDownLatch中的锁存器计数为0,就直接返回
                if (c == 0)
                    return false;
                // 如果状态不为0,则将状态减一
                int nextc = c-1;
                /*
                 * 更新state值,即锁存器计数值,通过CAS保证线程安全
                 * CAS操作有3个操作数,内存值M,预期值E,新值U,如果M==E,则将内存值修改为B,否则啥都不做
                 * compareAndSetState(c, nextc)方法:
                 *      c:表示预期值
                 *       nextc:要更新的值
                 *   在此处,表示c==getState()时返回true,并更新state值为nextc
                 *   如果c!=getState()时,表示已经有其他线程更新了state值,
                 *   所以这里不进行更新,直接返回false,通过死循环再重新获取最新的getState()值
                 */
                if (compareAndSetState(c, nextc))
                    // 返回状态是否为0判断,为0表示锁存器计数为0,可以唤醒await的进程了
                    // 这里唤醒进程的操作也是通过AQS进行实现的
                    return nextc == 0;
            }
        }
    }

 从源码可知 CountDownLatch 内部是通过 AQS 的共享模式实现的

3、await() 和 await(long timeout, TimeUnit unit) 方法

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

 此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。

public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
     return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非超时或者被中断。

4、countDown() 方法

此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程 

 /**   
     * count值减 1,直到计数达到零,释放所有等待的线程 。
     *      
     *  <p>如果当前计数大于零,则递减。
     *   如果新计数为零,则重新启用所有等待的线程 ,达到线程调度的目的。
     *      
     * <p>如果当前计数等于零,则没有任何反应。
     */    
public void countDown() {
    sync.releaseShared(1);
}

注意:

  1. 初始化CountDownLatch 时的计数器必须大于0,只有当计数器等于 0 的时候,调用 await() 方法是不会阻塞。
  2. 任务的结束并不一定代表着正常的结束,有可能是在运算的过程中出现错误,因此为了能够正确地执行countDown(),需要将该方法的调用放在finally代码块中,否则就会出现主线程(任务)await()方法永远不会退出阻塞的问题。

猜你喜欢

转载自blog.csdn.net/small_love/article/details/111186796