运动会上的CountDownLatch

用百米赛跑展示CountDownLatch的用法

Posted by Jeremy Song on 2022-06-24
Estimated Reading Time 5 Minutes
Words 1.4k In Total
Viewed Times

田径运动会

点开这篇文章的你,突然感觉眼前一晃,我们已经穿越到了2008年的鸟巢,现在马上将要开始的项目的正是田径之王——百米短跑。

身为Coder的你突然想到,如果用代码来模拟赛跑,应该怎么实现呢?
于是你想到了使用线程来模拟每个运动员,每个线程都运行结束则表示选手都到达了终点,比赛结束。

那么,怎么来处理每个选手都完成比赛了呢。反正我是笨笨的想到了用 AtomicInteger 共享变量来记录已完成的选手数,等这个数等于选手数的时候就表示选手们已经全部完成比赛了。

但这个看起来是那么的不优雅。

重头戏(多唤醒一)

接下来重点来了。

CountDownLatch —— Java AQS(AbstractQueuedSynchronizer) 中的一员大将前来报道。这个家伙可就是专门来处理上面这个情况的。

看下面这个代码:

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
39
40
41
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Test1 {
private static final int TOTAL = 10;

public static void main(String[] args) throws InterruptedException {
System.out.println("裁判员:比赛开始~ GO!");
CountDownLatch cdl = new CountDownLatch(TOTAL);
for (int i = 1; i <= TOTAL; i++) {
new Thread(new Task(cdl, i)).start();
}
cdl.await();
System.out.println("裁判员:比赛结束!");
}

private static class Task implements Runnable {
Task(CountDownLatch cdl, int idx) {
this.cdl = cdl;
this.idx = idx;
}

private CountDownLatch cdl;
private int idx;

@Override
public void run() {
try {
// 模拟选手准备耗时
Thread.sleep(new Random((long) (Math.random() * Integer.MAX_VALUE)).nextLong(1000 * 3));
System.out.printf("--[%d]选手%d准备好了%n", System.currentTimeMillis(), idx);
// 模拟选手比赛耗时
Thread.sleep(new Random((long) (Math.random() * Integer.MAX_VALUE)).nextLong(1000 * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("选手%d已到达终点!%n", idx);
cdl.countDown();
}
}
}

上面的代码模拟了百米赛跑的场景,来让我们运行一下代码~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
裁判员:比赛开始~  GO!
--[1655357416735]选手1准备好了
--[1655357416899]选手9准备好了
--[1655357417090]选手4准备好了
--[1655357417174]选手7准备好了
--[1655357417737]选手8准备好了
选手8已到达终点!
--[1655357418568]选手6准备好了
--[1655357418646]选手5准备好了
--[1655357418897]选手3准备好了
--[1655357419150]选手2准备好了
选手3已到达终点!
--[1655357419343]选手10准备好了
选手7已到达终点!
选手6已到达终点!
选手1已到达终点!
选手9已到达终点!
选手5已到达终点!
选手4已到达终点!
选手10已到达终点!
选手2已到达终点!
裁判员:比赛结束!

欸~ 这个结果怎么看起来怪怪的(多线程问题,如果你的结果不是这种类似的,可以多运行几次)

改进(一唤醒多)

怎么会存在有的选手还没准备好,有的选手就已经到达终点了呢。

由于每个选手的准备时间不一样,那我们也应该在选手都准备好之后,才打响发令枪。
这样才和实际的情况比较吻合。OK,修改代码如下:

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
39
40
41
42
43
44
45
46
47
48
import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Test2 {
private static final int TOTAL = 10;

public static void main(String[] args) throws InterruptedException {
System.out.println("裁判员:比赛开始~ READY!");
CountDownLatch single = new CountDownLatch(1);
CountDownLatch cdl = new CountDownLatch(TOTAL);
for (int i = 1; i <= TOTAL; i++) {
new Thread(new Task(single, cdl, i)).start();
}
Thread.sleep(1000 * 5);
System.out.println("裁判员:GO!");
single.countDown();
cdl.await();
System.out.println("裁判员:比赛结束!");
}

private static class Task implements Runnable {
Task(CountDownLatch single, CountDownLatch cdl, int idx) {
this.single = single;
this.cdl = cdl;
this.idx = idx;
}

private CountDownLatch single;
private CountDownLatch cdl;
private int idx;

@Override
public void run() {
try {
// 模拟选手准备耗时
Thread.sleep(new Random((long) (Math.random() * Integer.MAX_VALUE)).nextLong(1000 * 3));
System.out.printf("--[%d]选手%d准备好了%n", System.currentTimeMillis(), idx);
single.await();
// 模拟选手比赛耗时
Thread.sleep(new Random((long) (Math.random() * Integer.MAX_VALUE)).nextLong(1000 * 10));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("选手%d已到达终点!%n", idx);
cdl.countDown();
}
}
}

我们再次运行一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
裁判员:比赛开始~  READY!
--[1655358774480]选手2准备好了
--[1655358774595]选手9准备好了
--[1655358774985]选手8准备好了
--[1655358775160]选手5准备好了
--[1655358775271]选手10准备好了
--[1655358775534]选手1准备好了
--[1655358775733]选手7准备好了
--[1655358776142]选手4准备好了
--[1655358776358]选手3准备好了
--[1655358777281]选手6准备好了
裁判员:GO!
选手3已到达终点!
选手4已到达终点!
选手6已到达终点!
选手8已到达终点!
选手7已到达终点!
选手10已到达终点!
选手5已到达终点!
选手1已到达终点!
选手2已到达终点!
选手9已到达终点!
裁判员:比赛结束!

这次的结果终于和预期一样了,不管你执行多少次代码模拟,也不会出现上面那种情况了。

OK,根据这个例子的两段代码 CountDownLatch 的使用场景就已经讲完了,至于原理吗? 这个就直接看源码去吧,这里就不多啰嗦啦!

最后

最后在提一下:

1
2
3
4
5
6
7
public class CountDownLatch {
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// ...
}

上面这个构造方法中的 count 可简单理解为,调用countDown()的次数达到这个值就会唤醒await,让阻塞的进程继续运行下去
好了,本篇结束~ 撒花★,°:.☆( ̄▽ ̄)/$:.°★


欢迎关注我的公众号 须弥零一,跟我一起学习IT知识。


如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !