如何让多线程步调一致

162 阅读6分钟

如何让多线程步调一致

场景引入

假设存在业务场景,电商系统有一个对账系统,该对账系统就是先查询订单库再查询派送单库,然后执行对账逻辑,将它们的差异写入差异库,逻辑如下所示。

将场景代码化如下:

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

这种写法在数据量少的情况是没有问题的,但如果订单库和派送单库的未对账数据量大,这将是性能瓶颈所在,如何解决呢?加入多线程操作,因为订单库和派送单库之间并没有依赖关系,改进代码如下。

 while(存在未对账订单){
     Thread T1 = new Thread(()->{
         // 查询订单
         pos = getPOrders();
     });
 ​
     Thread T2 = new Thread(()->{
         // 查询派送单
         dos = getDOrders();
     });
     T1.start();
     T2.start();
     T1.join();
     T2.join();
     // 执行对账逻辑
     diff = check(pos, dos);
     // 差异数据入库
     save(diff);
 }

CountDownLatch线程同步

这种写法实现了多线程写法,但是每次需要创建线程显然是对性能的一种消耗,那就将线程改为线程池,那这里存在一个问题,如何知道线程池中的线程执行完毕呢?利用上诉写法虽然会创建线程但是能利用join方法能够保证查询订单和派送单逻辑执行完毕,线程池却无法做到,如何处理呢?

采用管程实现,定义一个共享变量X=2,查询已对账订单执行完毕就减一,查询已对账派送单结束也减一,然后将条件变量设置为X=0,这种解决思路虽好,但是我们不需要重复造轮子,可以采用SDK提供给我们的方法CountDownLatch

代码改造如下

 ExecutorService executorService = Executors.newFixedThreadPool(2);
 while(存在未对账订单){
     // 创建同步方法对象 相当于管程实现中定义共享变量X=2
     CountDownLatch countDownLatch = new CountDownLatch(2);
 ​
     executorService.execute(()->{
         // 查询订单
         pos = getPOrders();
         // 相当于管程实现中将X减一操作
         countDownLatch.countDown();
     });
 ​
     executorService.execute(()->{
         // 查询派送单
         dos = getDOrders();
         // 相当于管程实现中将X减一操作
         countDownLatch.countDown();
     });
     // 相当于管程实现中条件变量X=0条件
     countDownLatch.await();
     // 执行对账逻辑
     diff = check(pos, dos);
     // 差异数据入库
     save(diff);
 }

到这里性能的初步优化就已经完成,还能进一步优化性能吗?当然可以目前优化的只是查询订单和查询派送单逻辑并行,两个查询和对账逻辑,差异数据入库还是串行的,如果在执行对账逻辑的时候,就可以执行下一轮的查询逻辑,那性能是不是更好呢?过程如下图所示

那应该如何实现呢?

两次查询操作能够和对账逻辑并行操作,并且对账逻辑依赖查询操作的结果,也就是查询完应该通知对账逻辑,这就是典型的生产者消费者模型,既然符合生产者消费者模型那么需要有一个存储生产者生产的数据队列。

但是对于上诉需求,肯定是需要两个队列的,一个存储订单数据一个存储派送单数据,两个队列应该存在对应关系,如图所示

查询一次订单数据就向存储订单队列插入一条数据,查询一次派送单数据就向存储派送单队列插入一条数据,对账逻辑每次都从订单队列和派送单队列出队一个数据,用于计算对账逻辑。

如何能实现两个队列完全并行呢?线程T1生产一条数据,同时插入订单队列,线程T2生产一条数据,同时插入派送单队列,通知线程T3执行对账逻辑,这里就隐藏了一个问题,如何能保证线程T1,线程T2需要保证步调一致,不能一个太快一个太慢这样就保证不了两个队列的对应关系,那如何去保证两个相互等待,同步执行呢?

CyclicBarrier实现线程同步

上面提到的两个线程相互等待,同步执行也可以使用管程实现,怎么做呢?

定义一个共享变量X=2,线程T1查询订单数据,线程T2查询派送单数据,线程T1,T2查询完数据后,判断共享变量X是否大于0,如果大于就将T1或者T2睡眠,如果共享变量X等于0通知线程T3执行对账逻辑,同时唤醒T1或者T2,同时将变量X重置为2。

同样在真实开发中不建议这样做,SDK有提供方法,也就是CyclicBarrier。

修改代码如下

 class CheckOrder{
     // 订单队列
     Vector<P> pos = new Vector<P>();
     // 派送单队列
     Vector<D> dos = new Vector<D>();
     ExecutorService executorService = Executors.newSingleThreadExecutor();
 ​
     // 创建同步方法对象 相当于管程实现中定义共享变量X=2
     CyclicBarrier cyclicBarrier = new CyclicBarrier(2,()->{
         // 必须使用线程池做,线程池实现异步对账入库,非阻塞
         executorService.execute(()->{
             // CyclicBarrier 回调方法。当await等待线程数 到达指定的parties值后
             check();
         });
     });
 ​
     public void check(){
         P p = pos.remove(0);
         D d = dos.remove(0);
         // 执行对账逻辑
         diff = check(p, d);
         // 差异数据入库
         save(diff);
     }
 ​
     public void queryOrder(){
         Thread T1 = new Thread(()->{
             while(存在未对账订单){
                 // 查询订单
                 P p = getPOrders();
                 try {
                     // 相当于管程实现中将X减一操作
                     cyclicBarrier.await();
                 } catch (Exception e) {
                     e.printStackTrace();
                 } 
                 pos.add(p);
             }
         });
         T1.start();
         Thread T2 = new Thread(()->{
             while(存在未对账订单){
                 // 查询派送单
                 D d = getDOrders();
                 // 相当于管程实现中将X减一操作
                 try {
                     cyclicBarrier.await();
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 dos.add(d);
             }
         });
         T2.start();
     }
 }

当调用CheckOrder的queryOrder方法,开始进入对账逻辑,T1,T2线程相互等待,当计数器值从定义的2减为0后,会自动重置计数器的值为2唤醒线程T1,T2,同时调用CyclicBarrier定义的回调方法,通过线程池异步执行check方法实现线程相互等待逻辑。

CountDownLatch与CyclicBarrier差异

CountDownLatch与CyclicBarrier两个工具类都可以去实现线程同步,但是功能上还是有所差异

  • CountDownLatch是一个线程等待多个线程,类似于旅游团需要等待所有人一起上车,人没到齐团长不让开车。而CyclicBarrier是线程间的相互等待,不存在指定一个线程去等待其余线程,类似于多个驴友一起去旅游,谁先到谁就等待直到人全部到齐。
  • CountDownLatch的计数器不能重复使用,CyclicBarrier的计数器是可以重置的,可以重复使用。
  • CountDownLatch不存在回调函数,CyclicBarrier存在回调函数内容丰富。
  • CountDownLatch底层实现是AQS相对复杂,CyclicBarrier底层就是Lock和Condition相对简单,如果想要看底层源码的可以先从CyclicBarrier入手,这两个工具类的实现逻辑相差不大。

\