CyclicBarrier和CountDownLaunch极为相似,只不过CyclicBarrier功能更强,可以循环执行,并且一次循环后还可以执行指定的回调函数。
业务场景:
- 每日对账,对比实物库存和卖出数量:
- 1,获取实物库存(RPC调用仓库信息)
- 2,获取卖出数量(RPC调用订单信息)
- 3,对比两者之间的差,是否存在超卖
普通情况大家会这么写:
今日实际卖出数 = 调用仓库(skuId)
今日订单查询商品数 = 调用订单履约域(skuId)
if(今日实际卖出数 != 今日订单查询商品数){
存入数据库或打log,记录异常。
}
这么写的话是单线程执行,效率略低,因为两次RPC调用都比较耗时,就要等。而且查询回来后的结果对比的同时完全可以进行下一个skuId的查询,这样效率会成倍提升。实现如下:
warehouseQueue //仓库查出的数据队列
orderQueue //履约域查出的数据队列
skuQueue//sku队列,至于如何保证两个线程查询同一个skuId自己想下,这里不做实现了。
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 2, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
CyclicBarrier barrier = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {//回调逻辑。
warehouseQueue.poll;
orderQueue.poll;
//对比数量,并落库打印日志。
}
});
executor.execute(()->{
while(skuQueue != null){
Long warehouseSellCount = getWarehouseSellCount(skuId);
warehouseQueue.put(warehouseSellCount);
barrier.await();
}
});
executor.execute(()->{
while(skuQueue != null){
Long orderSkuSellCount = getOrderSellCount(skuId);
orderQueue.put(orderSkuSellCount);
barrier.await();
}
});
两个线程如果一个先执行完会等待另一个线程也执行完,直到CyclicBarrier的构造中2变为0,这时要调用回调函数,然后CyclicBarrier就会重置0为2,继续下一轮逻辑。 以上代码,可以保证不会一个查询的过快一个查询的过慢,barrier.await()会等待另一个同伴执行完成。然后两个线程才会在进行下一轮操作。看似已经提高效率很高。但是存在一个问题。
问题: CyclicBarrier最后的回调方法由那个线程执行? 答:对于cyclicBarrier的回调函数,是由最后一个await的线程执行完的。
构造函数,parties是协同线程数量,barrierAction是回调任务
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
await方法:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
dowait的一段代码:
int index = --count; //每次await数量减1
// tripped,index开始可以从构造中看到,等于最初值,每次减一,最后为0。
if (index == 0) {
boolean ranAction = false;
try {//为0后,直接将回调任务barrierCommand赋值给command。
//由当前线程,也就是最后一个执行完的线程来执行回调函数。
final Runnable command = barrierCommand;
if (command != null)
//执行回调
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
nextGeneration方法会唤醒其他先执行完的线程,也可以看出CyclicBarrier是由lock-condition实现的。
有兴趣可以看源码或者下面的成员变量。
private void nextGeneration() {
// 唤醒全部线程用于执行下个循环
trip.signalAll();
// 将减为0的count还原回最初的数量parties
count = parties;
generation = new Generation();
}
CyclicBarrier的成员变量:
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
/** Condition to wait on until tripped */
private final Condition trip = lock.newCondition();
/** The number of parties */
private final int parties;
/* The command to run when tripped */
private final Runnable barrierCommand;
/** The current generation */
private Generation generation = new Generation();
/**
* Number of parties still waiting. Counts down from parties to 0
* on each generation. It is reset to parties on each new
* generation or when broken.
*/
private int count;
如果最后对比的逻辑也比较复杂,并且需要落库的话,需要很多时间,也浪费了两个线程的时间去进行下一轮的查询操作。是否可以更优化?
//这里可以启用一个单线程的线程池异步执行。这样就可以让两个线程继续去执行查询。
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, ()->{ executor.execute(()->compare()); });
当然以上只是简易代码,真正实现起来要考虑同步机制,例如从queue中取或增加需要加锁,另外查询的速度如果太快,比较的速度很慢,可以增加比较的线程池数量。这些这里就不一一覆盖了。
自己碰到的问题,大家可以注意一下:cyclicBarrier.getNumberWaiting() 返回的是当前执行到第几个,并不是还剩几个。