• 线程池和CountDownLatch结合使用详解


    一、CountDownLatch 初始

      CountDownLatch 中 count down 是倒数的意思,latch 则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch 的作用也是如此,在构造 CountDownLatch 的时候需要传入一个整数 n,在这个整数“倒数”到 0 之前,主线程需要等待在门口,而这个“倒数”过程则是由各个执行线程驱动的,每个线程执行完一个任务“倒数”一次。总结来说,CountDownLatch 的作用就是等待其他的线程都执行完任务,必要时可以对各个任务的执行结果进行汇总,然后主线程才继续往下执行。

            CountDownLatch 主要有两个方法:countDown() 和 await() 。countDown() 方法用于使计数器减一,其一般是执行任务的线程调用,await() 方法则使调用该方法的线程处于等待状态,其一般是主线程调用。这里需要注意的是,countDown() 方法并没有规定一个线程只能调用一次,当同一个线程调用多次 countDown() 方法时,每次都会使计数器减一;另外,await() 方法也并没有规定只能有一个线程执行该方法,如果多个线程同时执行 await() 方法,那么这几个线程都将处于等待状态,并且以共享模式享有同一个锁。如下是其使用示例:

    public class CountDownLatchExample {
      public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(5);
        Service service = new Service(latch);
        Runnable task = () -> service.exec();
    
        for (int i = 0; i < 5; i++) {
          Thread thread = new Thread(task);
          thread.start();
        }
    
        System.out.println("main thread await. ");
        latch.await();
        System.out.println("main thread finishes await. ");
      }
    }
    
    public class Service {
      private CountDownLatch latch;
    
      public Service(CountDownLatch latch) {
        this.latch = latch;
      }
    
      public void exec() {
        try {
          System.out.println(Thread.currentThread().getName() + " execute task. ");
          sleep(2);
          System.out.println(Thread.currentThread().getName() + " finished task. ");
        } finally {
          latch.countDown();
        }
      }
    
      private void sleep(int seconds) {
        try {
          TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    }

            

      在上面的例子中,首先声明了一个CountDownLatch对象,并且由主线程创建了5个线程,分别执行任务,在每个任务中,当前线程会休眠2秒。在启动线程之后,主线程调用了CountDownLatch.await()方法,此时,主线程将在此处等待创建的5个线程执行完任务之后才继续往下执行。如下是执行结果:

    Thread-0 execute task. 
    Thread-1 execute task. 
    Thread-2 execute task. 
    Thread-3 execute task. 
    Thread-4 execute task. 
    main thread await. 
    Thread-0 finished task. 
    Thread-4 finished task. 
    Thread-3 finished task. 
    Thread-1 finished task. 
    Thread-2 finished task. 
    main thread finishes await. 
    

      

            从输出结果可以看出,主线程先启动了五个线程,然后主线程进入等待状态,当这五个线程都执行完任务之后主线程才结束了等待。上述代码中需要注意的是,在执行任务的线程中,使用了 try...finally 结构,该结构可以保证创建的线程发生异常时 CountDownLatch.countDown() 方法也会执行,也就保证了主线程不会一直处于等待状态。

    二、工作中使用 CountDownLatch 解决问题

     1)定义一个线程池

    public class ThreadUtils {
        
        private static ExecutorService executor;
    
        static {
            /**
             * 构建一个线程池
             * 获取服务器CPU的核数:Runtime.getRuntime().availableProcessors()
             * 线程池定义大小:CPU * 2 + 1
             */
            executor = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2 + 1,
                    Runtime.getRuntime().availableProcessors() * 2 + 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue(10000));
        }
        /**
         * 线程池中线程执行任务
         */
        public static void execute(Runnable task) {
            executor.execute(task);
        }
    
    }
    

     2)线程池结合 CountDownLatch 进行任务分批并行处理

    /**
    * 模拟线程池分批处理任务,主线程需要等待子任务线程执行完,结果汇总之后,主线程继续往下执行
    */
    public void handleLogin(List<String> paramList) {
    // 使用线程池中线程分批处理业务逻辑,并行处理任务提高终端响应速度
    CountDownLatch latch = new CountDownLatch(paramList.size());
    for (String param : paramList) {
    ThreadUtils.execute(() -> {
    try {
    log.info("业务逻辑处理,参数:{}", param);
    // 业务逻辑正常处理......
    } catch (Exception e) {
    log.error("调用下游系统出现错误,异常逻辑处理......");
    } finally {
    // 业务逻辑处理完毕,计数器减一【当前线程处理任务完毕,线程释放进入线程池,等待处理下一个任务】
    latch.countDown();
    }
    });
    }
    // 主线程需要等待子任务线程执行完,结果汇总之后,主线程继续往下执行
    try {
    latch.await();
    } catch (Exception e) {
    log.error("等待超时", e);
    throw new RuntimeException("系统处理超时,请稍后再试");
    }
    }

    三、CountDownLatch 使用场景

            场景一:CountDownLatch 非常适合于对任务进行拆分,使其并行执行,比如某个任务执行2s,其对数据的请求可以分为五个部分,那么就可以将这个任务拆分为5个子任务,分别交由五个线程执行,执行完成之后再由主线程进行汇总,此时,总的执行时间将决定于执行最慢的任务,平均来看,还是大大减少了总的执行时间。

            场景二:使用 CountDownLatch 的地方是使用某些外部链接请求数据的时候,比如图片。在本人所从事的项目中就有类似的情况,因为我们使用的图片服务只提供了获取单个图片的功能,而每次获取图片的时间不等,一般都需要1.5s~2s。当我们需要批量获取图片的时候,比如列表页需要展示一系列的图片,如果使用单个线程顺序获取,那么等待时间将会极长,此时我们就可以使用CountDownLatch对获取图片的操作进行拆分,并行的获取图片,这样也就缩短了总的获取时间。

  • 相关阅读:
    数据库学习笔记01 Mariadb安装配置
    数据库学习笔记02 SQL 增删改查
    EF oral study notes 02 level 12~13
    Python编程学习基础笔记10
    EF oral study notes 01 level 10~11
    Linux基础操作命令
    20201318李兴昕第11章学习笔记
    20201318李兴昕第九章学习笔记
    20201318李兴昕第七、八章学习笔记
    20201318李兴昕第十章学习笔记
  • 原文地址:https://www.cnblogs.com/blogtech/p/16270225.html
Copyright © 2020-2023  润新知