CountDownLatch和CycliBarrier本质其实就是计数器。具体的区别在于:
CountDownLatch主要用于解决一个线程等待多个线程的场景。
CycliBarrier是一组线程之间互相等待。
除此之外,CountDownLatch的计数器是不能循环利用的,也就是说一旦计数器减到0,再有线程调用await(),该线程会直接通过。但CycliBarrier的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到0会自动重置到你设置的初始值,此外还可以设置回调函数。
无论是物流还是电商,亦或是金融互联网,免不了谈的,就一点。如何确保账务正确。下面介绍一种常见的业务场景:比如用户通过在线商城进行下单,会生成电子订单,保存在订单库;之后物流会生成派送单给用户发货,派送单保存在派送单库,当然这个数据来源肯定是不同的系统,如物流系统告知我们的这样的。为了防止漏派或者重复派送,对账系统每天还会校验是否存在异常订单,具体业务先不展开描述。
下面看下对账的基本处理逻辑,当然行业不同,处理方案也不尽相同,但都大同小异。目前对账系统的处理是首先查询订单,然后派送订单,之后对比订单和派送单,将不同的部分写入差异库中。下面分别说下对账系统抽象后的代码:
1、一般的处理流程:
通常是在一个while循环,如果存在未对账的订单,那么就去:① 查询未对账的订单、② 查询派送单、③ 执行对账、④ 入库差异。
2 、利用并行优化对账系统:
对于一般串行化的系统,性能优化首先想到的是能否利用多线程并行处理。
在这个场景来说,①查询未对账与②查询派送单是否可以并行处理呢?显然是可以的,因为这两个操作没有先后顺序的依赖。这两个最耗时的操作并行后,你会发现同等时间内,并行执行的吞吐量近乎单线程的2倍,优化效果还是相对明显的。
来说说具体的代码,我们可以新建两个线程来执行①和②的操作,之后主线程在T1和T2的join来实现等待,当T2和T2线程退出时,调用的主线程就会从阻塞态被唤醒,从而执行之后的③和④。
3、利用CountDownLatch实现线程等待:
可能你也发现了,while循环里面每次都会创建新的线程,而创建线程可是个耗时的操作。所以,最好是创建出来的线程能够循环利用,这个时候就可以使用线程池了。
我们可以在while循环外面,创建固定大小为2的线程池,之后在while循环里用,但是新的问题又出现了,我们之前是主线程中使用两个线程的join()来等待线程的退出,但是线程池的方案中,这两个线程根本就不会退出,所以join方法失效了。
最简单的方法就是整一个计数器,执行完①②的操作后,计数器减为0。在主线程里,等待计数器等于0时,执行③④的操作。等待计数器其实就是一个条件变量,用管程实现也简单。
因为Java中并发包已经提供了实现功能的工具类:CountDownLatch,在执行完①②的操作后,在主线程中使用latch.await()实现对计数器等于0的等待。
4 、进一步去优化性能,双队列实现完全并行:
有小伙伴不难看出,前面两个查询操作已经并行了,但是③和④还是串行的,其实可以在执行对账的时候,可以同时去查询下一轮的查询操作。
两次查询操作能够与对账实现并行,对账还依赖查询后的结果,明显有点生产者-消费者的意思了。我们完全可以通过两个队列、来分别保存查询的数据,即未对账订单和派送单。
但是T1与T2这两个线程也得保持步调一致,即同时当T1和T2都生产完查询所需的数据后,再通知线程T3来进行对账的操作。
使用CyclicBarrier来实现计数同步,当然你也可以自己利用一个计数器来操作,计数器初始化为2,线程T1和T2生产完一条数据都将计数器减1,如果计数器大于0则线程T1或者T2等待。如果计数器等于0,则通知线程T3,并唤醒等待的T1或者T2,与此同时,将计数器重置为2,这样T1和T2生产下一条数据的时候就可以继续使用这个计数器了。
我们推荐你使用CyclicBarrier计数器,在初始化的同时,还可以传入一个回调函数,这样当计数器减为0时,会调用这个回调函数。同时CyclicBarrier还有自动重置的功能,当减到0的时候,会自动重置你设置的初始值,实在是方便多了。
下面给出最终的示例代码,欢迎大家多多探讨交流!需要注意的是,CyclicBarrier会在你的回调函数执行完才会唤醒等待的线程。
// 订单队列
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();
}