资讯专栏INFORMATION COLUMN

用CountDownLatch提升请求处理速度

oujie / 3257人阅读

摘要:是多线程包里的一个常见工具类,通过使用它可以借助线程能力极大提升处理响应速度,且实现方式非常优雅。主线程处于状态,直到的值数减到,则主线程继续执行。此时必须使用线程池,并限定最大可处理线程数量,否则服务器不稳定性会大福提升。

countdownlatch是java多线程包concurrent里的一个常见工具类,通过使用它可以借助线程能力极大提升处理响应速度,且实现方式非常优雅。今天我们用一个实际案例和大家来讲解一下如何使用以及需要特别注意的点。

由于线程类的东西都比较抽象,我们换一种讲解思路,先讲解决问题的案例,然后再解释下原理。

假设在微服务架构中,A服务会调用B服务处理一些事情,且每处理一次业务,A可能要调用B多次处理逻辑相同但数据不同的事情。为了提升整个链路的处理速度,我们自然会想到是否可以把A调用B的各个请求组成一个批次,这样A服务只需要调用B服务一次,等B服务处理完一起返回即可,省了多次网络传输的时间。代码如下:

/**
 * 批次请求处理服务
 * @param batchRequests 批次请求对象列表
 * @return
 */
public List deal(List batchRequests){
  List resultList = new ArrayList<>();
  if(batchRequests != null){
    for(DealRequest request : batchRequests){
      //遍历顺序处理单个请求
      resultList.add(process(request));
    }
  }
  return resultList;
}

但是B服务顺序处理批次里每一个请求的时间并没有节省,假设批次里有3个请求,一个请求平均耗时100MS,则B服务还是要花费300MS来处理完。有什么办法能立刻简单提升3倍处理速度,令总花费时间只需要100MS?到我们的大将countdownlatch出场了!代码如下:

/**
 * 使用countdownlatch的批次请求处理服务
 * @param batchRequests 批次请求对象列表
 * @return
 */
public List countDownDeal(List batchRequests){

  //定义线程安全的处理结果列表
  List countDownResultList = Collections.synchronizedList(new ArrayList());

  if(batchRequests != null){

        //定义countdownlatch线程数,有多少个请求,我们就定义多少个
        CountDownLatch runningThreadNum = new CountDownLatch(batchRequests.size());

    for(DealRequest request : batchRequests){
      //循环遍历请求,并实例化线程(构造函数传入CountDownLatch类型的runningThreadNum),立刻启动
      DealWorker dealWorker = new DealWorker(request, runningThreadNum, countDownResultList);
      new Thread(dealWorker).start();
    }

        try {
          //调用CountDownLatch的await方法则当前主线程会等待,直到CountDownLatch类型的runningThreadNum清0
          //每个DealWorker处理完成会对runningThreadNum减1
          //如果等待1分钟后当前主线程都等不到runningThreadNum清0,则认为超时,返回false,可以根据实际情况选择处理或忽视
            runningThreadNum.await(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
          //此处简化处理,非正常中断应该抛出异常或返回错误结果
            return null;
        }
  }
  return countDownResultList;
}

/**
 * 线程请求处理类
 *
 */
private class DealWorker implements Runnable {

      /** 正在运行的线程数 */
      private CountDownLatch  runningThreadNum;

      /**待处理请求*/
      private DealRequest request;

      /**待返回结果列表*/
      private List countDownResultList;

      /**
       * 构造函数
       * @param request 待处理请求
       * @param runningThreadNum 正在运行的线程数
       * @param countDownResultList 待返回结果列表
       */
      private  DealWorker(DealRequest request, CountDownLatch runningThreadNum, List countDownResultList) {
        this.request = request;
        this.runningThreadNum = runningThreadNum;
        this.countDownResultList = countDownResultList;
      }

  @Override
  public void run() {
    try{
      this.countDownResultList.add(process(this.request));
    }finally{
      //当前线程处理完成,runningThreadNum线程数减1,此操作必须在finally中完成,避免处理异常后造成runningThreadNum线程数无法清0
      this.runningThreadNum.countDown();
    }
  }
}

是不是很简单?下图和上面的代码又做了一个对应,假设有3个请求,则启动3个子线程DealWorker,并实例化值数等于3的CountDownLatch。每当一个子线程处理完成后,则调用countDown操作减1。主线程处于awaiting状态,直到CountDownLatch的值数减到0,则主线程继续resume执行。

在API中是这样描述的:
用给定的计数 初始化 CountDownLatch。由于调用了 countDown() 方法,所以在当前计数到达零之前,await 方法会一直受阻塞。之后,会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用 CyclicBarrier。
经典的java并发编程实战一书中做了更深入的定义:CountDownLatch属于闭锁的范畴,闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态。闭锁的作用相当于一扇门:在闭锁到达结束状态之前(上面代码中的runningThreadNumq清0),这扇门一直是关闭的,并且没有任何线程能通过(上面代码中的主线程一直await),当到达结束状态时,这扇门会打开并允许所有线程通过(上面代码中的主线程可以继续执行)。当闭锁到达结束状态后,将不会再改变状态,因此这扇门将永远保持打开状态。

像FutureTask,Semaphore这类在concurrent包里的类也属于闭锁,不过它们和CountDownLatch的应用场景还是有差别的,这个我们在后面的文章里再细说。

使用CountDownLatch有哪些需要注意的点

批次请求之间不能有执行顺序要求,否则多个线程并发处理无法保证请求执行顺序

各线程都要操作的结果列表必须是线程安全的,比如上面代码范例的countDownResultList

各子线程的countDown操作要在finally中执行,确保一定可以执行

主线程的await操作需要设置超时时间,避免因子线程处理异常而长时间一直等待,如果中断需要抛出异常或返回错误结果

使用CountDownLatch提高批次处理速度的问题

如果一个批次请求数很多,会瞬间占用服务器大量线程。此时必须使用线程池,并限定最大可处理线程数量,否则服务器不稳定性会大福提升。

主线程和子线程间的数据传输变得困难,稍不注意会造成线程不安全的问题,且代码可读性有一定下降

下一篇文章我们讲讲FutureTask的应用场景,谢谢!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67666.html

相关文章

  • 长文慎入-探索Java并发编程与高并发解决方案

    摘要:所有示例代码请见下载于基本概念并发同时拥有两个或者多个线程,如果程序在单核处理器上运行多个线程将交替地换入或者换出内存这些线程是同时存在的,每个线程都处于执行过程中的某个状态,如果运行在多核处理器上此时,程序中的每个线程都 所有示例代码,请见/下载于 https://github.com/Wasabi1234... showImg(https://upload-images.jians...

    SimpleTriangle 评论0 收藏0
  • java高并发系列 - 第21天:java中的CAS操作,java并发的基石

    摘要:方法由两个参数,表示期望的值,表示要给设置的新值。操作包含三个操作数内存位置预期原值和新值。如果处的值尚未同时更改,则操作成功。中就使用了这样的操作。上面操作还有一点是将事务范围缩小了,也提升了系统并发处理的性能。 这是java高并发系列第21篇文章。 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库...

    zorro 评论0 收藏0
  • Java多线程编程实战:模拟大量数据同步

    摘要:所以得出结论需要分配较多的线程进行读数据,较少的线程进行写数据。注意多线程编程对实际环境和需求有很大的依赖,需要根据实际的需求情况对各个参数做调整。 背景 最近对于 Java 多线程做了一段时间的学习,笔者一直认为,学习东西就是要应用到实际的业务需求中的。否则要么无法深入理解,要么硬生生地套用技术只是达到炫技的效果。 不过笔者仍旧认为自己对于多线程掌握不够熟练,不敢轻易应用到生产代码中...

    elliott_hu 评论0 收藏0
  • BATJ都爱问的多线程面试题

    摘要:今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用可以禁止的指令重排,保证在多线程环境下也能正常运行。 下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线...

    高胜山 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<