并发包工具类:CyclicBarrier、CountDownLatch

场景:对账系统最近越来越慢,老板让优化,用户通过在线商城下单,会生成电子订单,保存在订单库;之后物流会生成派送单给用户发货,派送单保存在派送单库。为了防止漏派送或者重复派送,对账系统每天还会校验是否存在异常订单。

并发包工具类:CyclicBarrier、CountDownLatch_第1张图片
image

对账系统代码抽象:

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

单线程改成多线程并发执行。
我们创建了两个线程T1和T2,并行执行查询未对账订单getPOrders()和查询派送单getDOrders()这两个操作。在主线程中执行对账操作check()和差异写入save()两个操作。

主线程需要等待线程T1和T2执行完才能执行check()和save()这两个操作,为此我们通过调用T1.join()和T2.join()来实现等待,当T1和T2线程退出时,调用T1.join()和T2.join()的主线程就会从阻塞态被唤醒,从而执行之后的check()和save()。

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);
} 
CountDownLatch实现线程等待

在while循环里面,我们首先创建了一个CountDownLatch,计数器的初始值等于2,之后在pos = getPOrders();和dos = getDOrders();两条语句的后面对计数器执行减1操作,这个对计数器减1的操作是通过调用 latch.countDown(); 来实现的。在主线程中,我们通过调用 latch.await() 来实现对计数器等于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);
}
再次思考:

两次查询操作能够和对账操作并行,对账操作还依赖查询操作的结果,这明显有点生产者-消费者的意思,两次查询操作是生产者,对账操作是消费者。既然是生产者-消费者模型,那就需要有个队列,来保存生产者生产的数据,而消费者则从这个队列消费数据。

一个线程T1执行订单的查询工作,一个线程T2执行派送单的查询工作,当线程T1和T2都各自生产完1条数据的时候,通知线程T3执行对账操作。

隐藏条件,就是线程T1和线程T2的工作要步调一致,不能一个跑得太快,一个跑得太慢,只有这样才能做到各自生产完1条数据的时候,通知线程T3。

CyclicBarrier实现线程同步

创建了一个计数器初始值为2的CyclicBarrier,创建CyclicBarrier的时候,我们还传入了一个回调函数,当计数器减到0的时候,会调用这个回调函数。

线程T1负责查询订单,当查出一条时,调用 barrier.await() 来将计数器减1,同时等待计数器变成0;线程T2负责查询派送单,当查出一条时,也调用 barrier.await() 来将计数器减1,同时等待计数器变成0;当T1和T2都调用 barrier.await() 的时候,计数器会减到0,此时T1和T2就可以执行下一条语句了,同时会调用barrier的回调函数来执行对账操作。

CyclicBarrier的计数器有自动重置的功能,当减到0的时候,会自动重置你设置的初始值。

// 订单队列
Vector

pos; // 派送单队列 Vector 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(); }

总结:

CountDownLatch主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有的游客到齐才能去下一个景点;而CyclicBarrier是一组线程之间互相等待,更像是几个驴友之间不离不弃。除此之外CountDownLatch的计数器是不能循环利用的,也就是说一旦计数器减到0,再有线程调用await(),该线程会直接通过。但CyclicBarrier的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到0会自动重置到你设置的初始值。除此之外,CyclicBarrier还可以设置回调函数。

你可能感兴趣的:(并发包工具类:CyclicBarrier、CountDownLatch)