CountDownLatch & CyclicBarrier
CountDownLatch和CyclicBarrier是Java并发编程中用于任务编排的并发原语,它要解决的就是并发 - 等待的问题,在本文中,我们将以对账系统流程,来看看两者的使用场景。
对账系统流程
- 查询订单
- 查询派送单
- 对比订单和派送单,将差异写入差异库
原型代码实现
while(存在未对账订单){
// 查询未对账订单
pos = getPOrders();
// 查询派送单
dos = getDOrders();
// 执行对账操作
diff = check(pos, dos);
// 差异写入差异库
save(diff);
}
优化思路
单线程变多线程执行任务,提高并行度。
代码优化1
CountDownLatch
将1,2交给线程池并行执行,使用countDownLatch来实现1,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);
}
代码优化2
将3,4也交给多线程并行执行,我们引入双队列,订单和派送单完成后都插入到队列中,T1, T2保持一致。
3进行check操作,需要等待T1, T2先完成。
CyclicBarrier
用 CyclicBarrier 实现线程同步, 与CountDownLatch不同的是CyclicBarrier会自动重置。
计数器初始化为 2,线程 T1 和 T2 生产完一条数据都将计数器减 1,如果计数器大于 0 则线程 T1 或者 T2 等待。如果计数器等于0,则通知线程 T3,并唤醒等待的线程 T1 或者 T2,与此同时,将计数器重置为 2,这样线程 T1 和线程 T2 生产下一条数据的时候就可以继续使用这个计数器了
// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池
Executor executor =
Executors.newFixedThreadPool(1);
//用 CyclicBarrier 实现线程同步
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 是一组线程之间互相等待,且可以循环利用。