JUC中的线程同步器原理1-CountDownLatch

194 阅读2分钟

一、CountDownLatch

在开发环境中会有这么一个场景:在主线程中开启多个线程并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总

直接手动创建线程:

public class JoinCountDownLatch1 {
    // 创建一个CountDownLatch实例
    private static volatile CountDownLatch countDownLatch
            = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 对计数器-1
                    countDownLatch.countDown();
                }
                System.out.println("child thread1 done!");
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 对计数器-1
                    countDownLatch.countDown();
                }
                System.out.println("child thread2 done!");
            }
        });
        // 启动子线程
        thread1.start();
        thread2.start();
        System.out.println("wait all child thread over!");
        // 阻塞等待所有子线程运行完毕(countDownLatch的计数器减为0)
        countDownLatch.await();
        System.out.println("all child thread over!");
    }
}

image.png

使用线程池对线程进行管理

在项目实践中一般避免直接操作线程,而是使用ExecutorService线程池来管理。使用ExecutorService时传递的参数是Runnable或者Callable对象,此时无法直接调用这些线程的join()方法,这就需要使用CountDownLatch达到效果了:

public class JoinCountDownLatch2 {
    // 创建一个CountDownLatch实例
    private static volatile CountDownLatch countDownLatch =
            new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        // 将线程1加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException ex){
                    ex.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
                System.out.println("child thread1 done!");
            }
        });
        // 将线程2加到线程池
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try{
                    Thread.sleep(1000);
                } catch (InterruptedException ex){
                    ex.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
                System.out.println("child thread2 done!");
            }
        });
        System.out.println("wait all child thread over");
        // 阻塞等待子线程执行完毕
        countDownLatch.await();
        System.out.println("all child thread done!");
        // 关闭线程池
        executorService.shutdown();
    }
}

image.png

和join()的区别:

  1. 调用一个子线程的join()方法后,该线程会一直阻塞直到子线程运行完毕,而CountDownLatch使用计数器来运行子线程运行完毕或者在运行中递减计数,也就是CountDownLatch可以在子线程运行的任何时候让await()方法返回而不一定必须等到线程结束
  2. 使用线程池来管理线程时一般都是添加Runnable/Callable到线程池,这时候就无法再调用join方法
  3. 总的来说,就是CountDownLatch相比join方法对线程同步有更灵活的控制

CountDownLatch原理概述:

CountDownLatch使用AQS实现,使用AQS的状态变量来存放计数器的值。首先在初始化CountDownLatch时设置状态值(计数器值),当多个线程调用countDown()时实际是原子性递减AQS的状态值。当线程调用await()后当前线程会被放入AQS的阻塞队列等待计数器为0再返回。其他线程调用countDown()让计数器-1,当计数器值变为0的时候,当前线程还要调用AQS的doReleaseShared方法来激活由于调用await()而被阻塞的线程