一句话说清楚 CountDownLatch 和 CyclicBarrier 的区别

时间:2022-07-25
本文章向大家介绍一句话说清楚 CountDownLatch 和 CyclicBarrier 的区别,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

日常工作中,经常会碰到这样的场景:有时候数据量特别大,任务量特别多,我们通常会开启多线程去分批执行任务,在所有任务执行完了之后,再去执行接下来的作业。

这时候,会想到 java 并发包提供的基础工具类,其中 有 CountDownLatch 和 CyclicBarrier ,它们都是提供多线程环境的协调功能,但是具体有什么区别呢?

CountDownLatch 操作的是事件,阻塞足够多的次数即可,不管几个线程;而 CyclicBarrier 侧重点是线程,强调多个线程间互相等待,同时结束。

01

CountDownLatch 用法

从代码层面,CountDownLatch 的用法是:

// 设置10个计数
CountDownLatch countDownLatch = new CountDownLatch(10);
// 每次调用即可减1 
countDownLatch.countDown();
    
// 其他线程一直等待减到0后,才继续执行
countDownLatch.await()

从一个场景出发:

现在有订单库和派送单库,先查询订单,再查询派送单,之后对比订单和派送单,将差异写入差异库。

其中查询订单库和查询派送单库,可以用两个线程并行执行。都执行完了之后,才执行对账。

我们可以写一下伪代码:

Executor executor = Executors.newFixedThreadPool(2);
while(存在未对账订单) {
    // 计数器初始化为2
    CountDownLatch latch = new CountDownLatch(2);
    // 查询未对账订单
    executor.execute(() -> {
        pOrders = getPOrders();
        latch.countDown();
    });
    
    // 查询派送单
    executor.execute(()-> {
        dOrders = getDOrders();
        latch.countDown();
    });
    
    // 等待两个查询结束
    latch.await();
    
    // 执行对账操作
    diff = check(pOrders,dOrders);
    
    // 差异写入差异库
    save(diff);
}

我们使用只有2个核心线程的线程池,分别执行查询订单和派送单的操作,并且初始化了一个大小为 2 的 CountDownLatch,每次查询完后,都要 countDown();

主线程则一直等待减为 0 了之后,才开始继续往下执行。

02

CyclicBarrier 用法

从代码使用角度来说:

// 初始化值为5的栅栏
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
// 每个线程调用 await()
cyclicBarrier.await();
// 等到有 5 个线程都执行了 await() 之后,继续执行。
// 并且 栅栏的 计数器会自动重置为 5 ,可以接着用

然后我们模拟一个场景

在英雄联盟中,选好英雄之后,会等待所有 10 个玩家进度条都到 100% 才开始游戏,我们可以使用 CyclicBarrier 来模拟这个场景

public class CyclicBarrierTest {
    private final static ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(5);
    private final static CyclicBarrier BARRIER = new CyclicBarrier(10);
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            final String name = "玩家" + i;
            EXECUTOR_SERVICE.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000);
                        System.out.println(name + "已准备,等待其他玩家准备...");
                        BARRIER.await();
                        Thread.sleep(1000);
                        System.out.println(name + "已加入游戏");
                    } catch (InterruptedException e) {
                        System.out.println(name + "离开游戏");
                    } catch (BrokenBarrierException e) {
                        System.out.println(name + "离开游戏");
                    }
                }
            });
        }
        EXECUTOR_SERVICE.shutdown();
    }
}

最后,比较一下 CountDownLatch 和 CyclicBarrier 的不同点:

  • CountDownLatch 是不可以重置的,所以无法重用;而 CyclicBarrier 则没有这个限制,可以重用;
  • CountDownLatch 的基本操作组合是 countDown/await。调用 await 的线程阻塞等待 countDown 足够多的次数,不管你是在一个线程还是多个线程里 countDown,只要次数足够即可。
  • CyclicBarrier 的基本操作组合,则就是 await,当所有伙伴 (parties)都调用了 await,才会继续进行任务,并自动进行重置。
  • 注意,正常情况下,CyclicBarrier 的重置都是自动发生的,如果我们调用 reset 方法,但还有线程在等待,就会导致等待线程被打扰,抛出 BrokenBarrierException 异常。
  • CyclicBarrier 侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束。