• CountDownLatch和CyclicBarrier:如何让多线程步调一致?


    案例:对账系统的业务是这样的,用户通过在线商城下单,会生成电子订单,保存在订单库;之后物流会生成派送单给用户发货,派送单保存在派送单库。为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单。对账系统的处理逻辑很简单,系统流程图如下。目前对账系统的处理逻辑是首先查询订单,然后查询派送单,之后对比订单和派送单,将差异写入差异库。

    对上面的代码抽象就是这样的,就是在一个单线程里面循环查询订单、派送单,然后执行对账,最后将写入差异库。

     while(存在未对账订单){
       // 查询未对账订单
       pos = getPOrders();
       // 查询派送单
       dos = getDOrders();
       // 执行对账操作
       diff = check(pos, dos);
       // 差异写入差异库
       save(diff);
     }

    1)上面的系统现在执行很慢,该怎样优化来执行速度呢?

    • 目前是单线程的,那单线程的话我们就考虑是否可以用多线程来做。查询未对账订单和查询派送单这两个操作是可以并行处理的。

    2)实现查询对账订单和查询派送单并行执行的代码应该是怎样的?

     
     while(存在未对账订单){
       // 查询未对账订单
       Thread T1 = new Thread(()->{
         pos = getPOrders();
      });
       T1.start();
       // 查询派送单
       Thread T2 = new Thread(()->{
         dos = getDOrders();
      });
       T2.start();
       // 等待T1、T2结束
       T1.join();
       T2.join();
       // 执行对账操作
       diff = check(pos, dos);
       // 差异写入差异库
       save(diff);
     }
    • 我们在主线程中开了两个插队的线程,等这两个查询的插队线程执行完了,阻塞的主线程被唤醒,那么就可以执行对账还有写入差异库的操作了。

    3)思考一下,我们上面的代码还有没有优化的空间呢?

    • 我们每次进行新的查询的对账的时候,都要创建两个新的线程出来,我们知道创建线程是比较好费时间的。那思考一下可不可以用线程池来减少创建线程的开销呢。

       
       // 创建2个线程的线程池
       Executor executor =
         Executors.newFixedThreadPool(2);
       while(存在未对账订单){
         // 查询未对账订单
         executor.execute(()-> {
           pos = getPOrders();
        });
         // 查询派送单
         executor.execute(()-> {
           dos = getDOrders();
        });
         
         /* ??如何实现等待??*/
         
         // 执行对账操作
         diff = check(pos, dos);
         // 差异写入差异库
         save(diff);
       }  

    4)使用上面线程池的代码的话,我的join就不能调用了,那我的主线程就不知道什么时候两个查询操作执行完了,这个时候该怎么办?

    • 我们可以使用一个计数器,初始值呢设置为2,查询一次就减1,当两个查询执行完,那计数器就是0了,我们的主线程也就能被唤醒执行了。

    5)上面的方案是我们自己想出来的,那java其实提供了一个非常方便的实现我们上面方案的工具类CountDownLatch,那使用CountDownLatch怎样优化我们的代码呢?

    • 主线程的话我们调用await方法来阻塞,两个查询线程我们执行countDown方法,会减1。主线程当检测到为0时就可以执行了

     
     // 创建2个线程的线程池
     Executor executor =
       Executors.newFixedThreadPool(2);
     while(存在未对账订单){
       // 计数器初始化为2
       CountDownLatch latch =
         new CountDownLatch(2);
       // 查询未对账订单
       executor.execute(()-> {
         pos = getPOrders();
         latch.countDown();
      });
       // 查询派送单
       executor.execute(()-> {
         dos = getDOrders();
         latch.countDown();
      });
       
       // 等待两个查询操作结束
       latch.await();
       
       // 执行对账操作
       diff = check(pos, dos);
       // 差异写入差异库
       save(diff);
     }

     

    6)上面使用CountDownLatch和线程池的方案已经很不错了,在思考一下,我们的这个程序还能不能优化一下?

    • 我的对账操作和下一次查询操作其实是不影响的,那么他们之间是可以并发执行的。也就是我在进行本次对账的同时,是可以执行下一次的查询操作的。

    7)对账需要查询出数据来才可以执行,这种的话对应什么模型?

    • 生产者-消费者模型

    8)既然看出来了是生产者消费者模型,那就需要一个队列,生产者生产出来东西放到队列,消费者去队列当中取。但是针对我们上面的案例,一个队列的话肯会造成数据混乱,我们应该怎样设计?

    • 使用两个队列,两个队列间的元素还有对应关系。订单查询操作将订单查询结果插入订单队列,派送单查询操作将派送单插入派送单队列,这两个队列的元素之间是有一一对应的关系的。我们的对账操作每次从两个队列当中各取一个,这样数据肯定不会发生混乱。

       

    9)我们如何用代码来实现查询和对账之间的并行呢?

    • 使用三个线程,一个线程 T1 执行订单的查询工作,一个线程 T2 执行派送单的查询工作,当线程 T1 和 T2 都各自生产完 1 条数据的时候,通知线程 T3 执行对账操作。这个想法虽看上去简单,但其实还隐藏着一个条件,那就是线程 T1 和线程 T2 的工作要步调一致,不能一个跑得太快,一个跑得太慢,只有这样才能做到各自生产完 1 条数据的时候,通知线程 T3。

    10)我们上面的方案有哪些要解决的问题?怎样解决?

    • T1和T2要走的齐

    • 他们执行完之后要能通知到T3

    • 解决这两个问题的方案也很简单,还是搞一个计数器,初始化为2,T1执行完减1,T2执行完减1。当计数器值为0时,T3就可以执行了,T3执行的时候把我们计数器又重置为2,此时T1,T2又可以执行了。

    11)实际项目中java其实给了我们现成的实现上面方案的工具类CyclicBarrier,代码实现的怎样的?

    • CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值,所以他要带个循环

     
     // 订单队列
     Vector<P> pos;
     // 派送单队列
     Vector<D> dos;
     // 执行回调的线程池
     Executor executor =
       Executors.newFixedThreadPool(1);
     final CyclicBarrier barrier =
       new CyclicBarrier(2, ()->{
         executor.execute(()->check());
      });
       
     void check(){
       P p = pos.remove(0);
       D d = dos.remove(0);
       // 执行对账操作
       diff = check(p, d);
       // 差异写入差异库
       save(diff);
     }
       
     void checkAll(){
       // 循环查询订单库
       Thread T1 = new Thread(()->{
         while(存在未对账订单){
           // 查询订单库
           pos.add(getPOrders());
           // 等待
           barrier.await();
        }
      });
       T1.start();  
       // 循环查询运单库
       Thread T2 = new Thread(()->{
         while(存在未对账订单){
           // 查询运单库
           dos.add(getDOrders());
           // 等待
           barrier.await();
        }
      });
       T2.start();
     }

     

     

  • 相关阅读:
    结构体怎么组包发送
    开源语音代码eSpeak1.06 的移植到单片机的过程(二)之分析下speak.c 文件
    看看深圳的房价
    开源语音代码eSpeak1.06 的移植到单片机的过程(一)0之分析下espeak.c 文件
    开源语音代码eSpeak1.06 的学习入门
    利尔达模组CAT1 UIS8910指令的 TCP相关中文解释
    将博客搬至CSDN
    【原创】大叔问题定位分享(39)azkaban定期出现fullgc
    【原创】大叔经验分享(129)mac下启动MAT报错
    【原创】大数据基础之Doris(1)编译安装和启动
  • 原文地址:https://www.cnblogs.com/YXBLOGXYY/p/16079520.html
Copyright © 2020-2023  润新知