理清 CountDownLatch 与 CyclicBarrier 的区别

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

这句话不是很好理解,我们通过例子来进行说明,首先来看一下 CountDownLatch。我们可以简单将其理解为一个计数器,当初始化一个 count=n 的 CountDownLatch 对象之后,需要调用该对象的 countDown() 方法来对计数器进行减值,直到计数器为 0 的时候,等待该计数器的线程才能继续执行。但是需要注意的一点是,执行 countDown() 方法的线程在执行完减值之后,并不会因此而阻塞。真正阻塞等待事件的是调用 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
public class LatchDemo implements Runnable {

private static final CountDownLatch START = new CountDownLatch(1);
private static final CountDownLatch END = new CountDownLatch(3);

@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 LatchDemo(), "A").start();
new Thread(new LatchDemo(), "B").start();
new Thread(new LatchDemo(), "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 阻塞的是调用 await() 方法的线程,而执行 countDown() 的线程在执行完计数器计数之后并不会阻塞,而是继续往下执行。

再来看一下 CyclicBarrier,在初始化构造 CyclicBarrier 时也需要指定计数器大小,同时我们还可以选择性的指定一个 Runnable 接口,当计数器计数完成时会回调该接口。相对于 CountDownLatch 来说,参与到 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
31
32
public class CyclicBarrierDemo implements Runnable {

private CyclicBarrier cyclicBarrier;

public CyclicBarrierDemo(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"));
CyclicBarrierDemo cb = new CyclicBarrierDemo(barrier);
new Thread(cb, "A").start();
new Thread(cb, "B").start();
new Thread(cb, "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
37
38
public class AboutDinnerDemo implements Runnable {

private static CountDownLatch latch = new CountDownLatch(1);

private CyclicBarrier barrier;

public AboutDinnerDemo(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~"));
AboutDinnerDemo demo = new AboutDinnerDemo(barrier);
new Thread(demo, "A").start();
new Thread(demo, "B").start();
new Thread(demo, "C").start();
TimeUnit.SECONDS.sleep(3);
AboutDinnerDemo.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 来约束大家需要等到所有人都到了再一起开吃。所以虽然这两个组件都表示等待的语义,但是各自拥有属于自己不同的特性,只有理解了它们的区别之后才能在特定场景下选择正确的组件来实现相应的语义。


转载声明 : 版权所有,商业转载请联系作者,非商业转载请注明出处
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议
Powered by hexo & Theme by hiero   Copyright © 2015-2019 浙ICP备 16010916  号,指 · 间 All Rights Reserved.