CyclicBarrier学习记录

288 阅读4分钟

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() 返回的是当前执行到第几个,并不是还剩几个。