理清 CountDownLatch 与 CyclicBarrier 的区别

对于刚接触同步器的同学来说,CountDownLatch 和 CyclicBarrier 应该是两个比较容易混淆的组件,它们都能表示让多个线程等待某个特定事件的语义,不过在功能上还是存在一些差别,实际上它们的 主要区别在于参与的线程是否需要阻塞相互等待一起到达事件的位置,然后再继续向下执行 。官方对这两个组件的定义分别如下:

  • CountDownLatch : A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.
  • CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.

看了之后是不是更加迷惑?下面我们通过举例来理清这二者的区别。

CountDownLatch

首先来看一下 CountDownLatch,我们可以简单将其理解为一个计数器,当初始化一个 count=n 的 CountDownLatch 对象之后,需要调用该对象的 CountDownLatch#countDown 方法来对计数器进行减值,直到计数器为 0 的时候,等待该计数器的线程才能继续执行。但是需要注意的一点是,执行 CountDownLatch#countDown 方法的线程在执行完减值操作之后,并不会因此而阻塞。真正阻塞等待事件的是调用 CountDownLatch 对象 CountDownLatch#await 方法的线程,该线程一直会阻塞直到计数器计数变为 0 为止。

先来举一个学生考试的例子,一般的考试都是一群学生坐在一个教室里面,等到规定的时间一起开始答题。因为每个学生资历的不同,所以学生在答题完成时间上有先有后,我们允许学生提前交卷。每个考场都有一个考官,负责收发试卷,以及维护考场秩序,考官必须等到收齐所有考生的试卷之后才能离开。这里我们可以利用两个 CountDownLatch 对象来模拟整个过程,假设有一名考官和三个考生参与整个过程,而每个个体都可以看做是一个独立的线程,示例实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static final CountDownLatch START = new CountDownLatch(1);
private static final CountDownLatch END = new CountDownLatch(3);

private static class Student implements Runnable {

@Override
public void run() {
try {
// 等待考试
System.out.println(Thread.currentThread().getName() + " is waiting");
START.await();
// 开始考试
TimeUnit.SECONDS.sleep(RandomUtils.nextInt(1, 5));
System.out.println(Thread.currentThread().getName() + " finished");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 交卷
END.countDown();
System.out.println(Thread.currentThread().getName() + " is over");
}
}
}

public static void main(String[] args) throws Exception {
new Thread(new Student(), "A").start();
new Thread(new Student(), "B").start();
new Thread(new Student(), "C").start();
TimeUnit.SECONDS.sleep(3);
System.out.println("Start");
// 老师宣布开始考试
START.countDown();
// 等待学生交卷
END.await();
TimeUnit.SECONDS.sleep(1);
System.out.println("End");
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
A is waiting
B is waiting
C is waiting
Start
B finished
B is over
C finished
C is over
A finished
A is over
End

由上面的例子可以看到 CountDownLatch 阻塞的是调用 CountDownLatch#await 方法的线程,而执行 CountDownLatch#countDown 方法的线程在执行完计数器计数之后并不会阻塞,而是继续往下执行。

CyclicBarrier

下面继续来看一下 CyclicBarrier,在初始化构造 CyclicBarrier 时也需要指定计数器大小,同时我们还可以选择性的指定一个 Runnable 实现,当计数器计数完成时会回调执行该 Runnable 实现。相对于 CountDownLatch 来说,参与到 CyclicBarrier 中的线程执行计数器计数发生于 CyclicBarrier#await 方法中,不过这里的计数会阻塞当前线程直到计数变为 0,或阻塞被中断为止。

最终的效果就是所有参与的线程都会在计数器变为 0 时一起被唤醒,然后继续向下执行。另外相对于 CountDownLatch 的一次性使用而言,CyclicBarrier 对象是可以被重用的,你可以理解为当前计数器变为 0,且所有的参与阻塞的线程都被唤醒之后,计数器立刻又恢复到最初设置的计数值,从而能够被再次使用。

这里我们利用签到的过程来演示 CyclicBarrier 的使用。假设几个人需要一起跟团出去旅游,大家约定好时间、地点,集合一起出发,其中有一个导游负责整个旅行的签到和旅途安排。假定有一名导游和三个游客参与整个过程,而每个个体都可以看做是一个独立的线程,示例实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private static class Tourist implements Runnable {

private CyclicBarrier cyclicBarrier;

public Tourist(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}

@Override
public void run() {
try {
for (int i = 0; i < 2; i++) {
TimeUnit.SECONDS.sleep(RandomUtils.nextInt(1, 6));
System.out.println("[" + i + "] " + Thread.currentThread().getName() + " check in");
cyclicBarrier.await();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " is over");
}
}

public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Let's go"));
Tourist tourist = new Tourist(barrier);
new Thread(tourist, "A").start();
new Thread(tourist, "B").start();
new Thread(tourist, "C").start();
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
[0] C check in
[0] A check in
[0] B check in
Let's go
[1] C check in
[1] B check in
[1] A check in
Let's go
A is over
C is over
B is over

我们可以看到在所有人(线程)在都到达之前,其他人(线程)即使先到了也要阻塞等待,这里为了演示 CyclicBarrier 的可重用性,我们增加了一次循环。

最后我们再来通过一个约饭的例子将 CountDownLatch 和 CyclicBarrier 结合起来设计一个程序。假设一个宿舍有三个人约好晚上一起吃饭,这三个同学一个在寝室,一个在实验室,另外一个在公司实习,大家都约好下午 5 点出发赶往目的地,先到的需要等待,直到三个人都到了才开吃,示例实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static CountDownLatch latch = new CountDownLatch(1);

private static class Person implements Runnable {

private CyclicBarrier barrier;

public Person(CyclicBarrier barrier) {
this.barrier = barrier;
}

@Override
public void run() {
try {
String name = Thread.currentThread().getName();
System.out.println(name + " is ready to go");
latch.await();
System.out.println(name + " is on the way");
TimeUnit.SECONDS.sleep(RandomUtils.nextInt(1, 6));
System.out.println(name + " arrived");
barrier.await();
TimeUnit.SECONDS.sleep(RandomUtils.nextInt(1, 6));
System.out.println(name + " ate up");
} catch (Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws Exception {
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("Let's eat~"));
new Thread(new Person(barrier), "A").start();
new Thread(new Person(barrier), "B").start();
new Thread(new Person(barrier), "C").start();
TimeUnit.SECONDS.sleep(3);
latch.countDown();
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
A is ready to go
B is ready to go
C is ready to go
A is on the way
B is on the way
C is on the way
B arrived
C arrived
A arrived
Let's eat~
B ate up
A ate up
C ate up

上述例子中,我们利用 CountDownLatch 来约束大家(参与的线程)到点儿了一起出发,并用 CyclicBarrier 来约束大家需要等到所有人都到了再一起开吃。所以虽然这两个组件都表示等待的语义,但是各自拥有属于自己不同的特性,只有理解了它们的区别之后才能在特定场景下选择正确的组件来实现相应的逻辑。

总结

本文基于示例介绍了 CountDownLatch 和 CyclicBarrier 各自的用途,最后我们来总结一下 CountDownLatch 和 CyclicBarrier 的区别:

  1. CountDownLatch 用于阻塞当前 1 个或多个线程,其目的是让这些线程等待其它线程的执行完成。
  2. CyclicBarrier 用于阻塞当前多个线程,其目的是让这些线程彼此之间相互等待,当这些线程均到达屏障后再一起往下执行。
  3. CountDownLatch 是一次性的,而 CyclicBarrier 在使用完成之后允许被重置以复用。