主题
《面渣逆袭》Java并发编程 篇 · 第 5/7 章。原版 PDF(下载 / 打印)
39.CountDownLatch(倒计数器)了解吗?
CountDownLatch ,倒计数器,有两个常见的应用场景[ 18] :
场景1 :协调子线程结束动作:等待所有子线程运行结束
CountDownLatch 允许一个或多个线程等待其他线程完成操作。
例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之后,才能开打。

CountDownLatch模仿这个场景(参考[18]):创建大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都完成确认后,才可以继续运行。
java
在这段代码中,new CountDownLatch(5) 用户创建初始的latch数量,各玩家通过
countDownLatch.countDown() 完成状态确认,主线程通过countDownLatch.await() 等待。
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
Thread 大乔 = new Thread(countDownLatch::countDown);
Thread 兰陵王 = new Thread(countDownLatch::countDown);
Thread 安其拉 = new Thread(countDownLatch::countDown);
Thread 哪吒 = new Thread(countDownLatch::countDown);
Thread 铠 = new Thread(() -> {
try {
// 稍等,上个卫生间,马上到...
Thread.sleep(1500);
countDownLatch.countDown();
} catch (InterruptedException ignored) {}
});
大乔.start();
兰陵王.start();
安其拉.start();
哪吒.start();
铠.start();
countDownLatch.await();
System.out.println("所有玩家已经就位!");
}场景2 . 协调子线程开始动作:统一各线程动作开始的时机
王者游戏中也有类似的场景,游戏开始时,各玩家的初始状态必须一致。不能有的玩家都出完装了,有的才降生。
所以大家得一块出生,在

在这个场景中,仍然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。需要注意的是,各
玩家虽然都调用了start() 线程,但是它们在运行时都在等待countDownLatch 的信号,在信号未收到前,它们不会往下执行。
java
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread 大乔 = new Thread(() -> waitToFight(countDownLatch));
Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch));
Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
Thread 铠 = new Thread(() -> waitToFight(countDownLatch));
大乔.start();
兰陵王.start();
安其拉.start();
哪吒.start();
铠.start();
Thread.sleep(1000);
countDownLatch.countDown();
System.out.println("敌方还有5秒达到战场,全军出击!");
}
private static void waitToFight(CountDownLatch countDownLatch) {
try {
countDownLatch.await(); // 在此等待信号再继续
System.out.println("收到,发起进攻!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}CountDownLatch 的核心方法也不多:
await() :等待latch降为0;
boolean await(long timeout, TimeUnit unit) :等待latch降为0,但是可以设置超时时间。比如有玩家超时未确认,那就重新匹配,总不能为了某个玩家等到天荒地老。
countDown() :latch数量减1;
getCount() :获取当前的latch数量。40.CyclicBarrier(同步屏障)了解吗?
CyclicBarrier 的字面意思是可循环使用(Cyclic )的屏障(Barrier )。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
它和CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有CyclicBarrier ,自然是它有和CountDownLatch 不同的地方。
不知道你听没听过一个新人UP主小约翰可汗,小约翰生平有两大恨— — “ 想结衣结衣不依, 迷爱理爱理不理。” 我们来还原一下事情的经过:小约翰在亲政后认识了新垣结衣,于是决定第一次选妃,向结衣表白,等待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,发誓生平不娶,突然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,等待回应。

我们拿代码模拟这一场景,发现CountDownLatch 无能为力了,因为CountDownLatch 的使用是一次性的,无法重复利用,而这里等待了两次。此时,我们用CyclicBarrier 就可以实现,因为它可以重复利用。

运行结果:

CyclicBarrier最最核心的方法,仍然是await():如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中
断、屏障被拆除、屏障被重设等情况;
上面的例子抽象一下,本质上它的流程就是这样就是这样:

41.CyclicBarrier和CountDownLatch有什么区别?
两者最核心的区别[ 18] :
CountDownLatch 是一次性的,而CyclicBarrier 则可以多次设置屏障,实现重复利用;
CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自己的任务;而CyclicBarrier中的各个线程可以等待其他线程它们区别用一个表格整理:
| CyclicBarrier | CountDownLatch |
|---|---|
| CyclicBarrier 是可重用的,其中的线程会等待所有的线 程完成任务。届时,屏障将被拆除,并可以选择性地做 一些特定的动作。 | CountDownLatch 是一次性的,不同 的线程在同一个计数器上工作,直到 计数器为0 . |
| CyclicBarrier 面向的是线程数 | CountDownLatch 面向的是任务数 |
| 在使用CyclicBarrier 时,你必须在构造中指定参与协作 的线程数,这些线程必须调用await() 方法 | 使用CountDownLatch 时,则必须要 指定任务数,至于这些任务由哪些线 程完成无关紧要 |
| CyclicBarrier 可以在所有的线程释放后重新使用 | CountDownLatch 在计数器为0 时不能 再使用 |
| 在CyclicBarrier 中,如果某个线程遇到了中断、超时等 问题时,则处于await 的线程都会出现问题 | 在CountDownLatch 中,如果某个线 程出现问题,其他线程不受影响 |
42.Semaphore(信号量)了解吗?
Semaphore (信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
听起来似乎很抽象,现在汽车多了,开车出门在外的一个老大难问题就是停车 。停车场的车位是有限的,只能允许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和剩余的车位,车辆就可以驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字0 ,车辆就得等待。如果满了的停车场有车离开,那么显示牌就又变绿,显示空车位数量,等待的车辆就能进停车场。

我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore 的本质就是协调多个线程对共享资源的获
取。

我们再来看一个Semaphore 的用途:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有1 0 个,这时我们必须控制只有1 0 个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore 来做流量控制,如下:
java
public class SemaphoreTest {
private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool =
Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);
public static void main(String[] args) {
for (int i = 0; i < THREAD_COUNT; i++) {
threadPool.execute(new Runnable() {@ Override
java
public void run() {
try {
s.acquire();
System.out.println("save data");
s.release();
} catch (InterruptedException e) {
}
}
});
}
threadPool.shutdown();
}
}在代码中,虽然有3 0 个线程在执行,但是只允许1 0 个并发执行。Semaphore 的构造方法Semaphore
(i ntpermits)接受一个整型的数字,表示可用的许可证数量。**Semaphore (1 0 )**表示允许1 0 个线程获取许可证,也就是最大并发数是1 0 。Semaphore 的用法也很简单,首先线程使用 Semaphore 的
acquire()方法获取一个许可证,使用完之后调用release()方法归还许可证。还可以用tryAcquire()方法尝试获取许可证。
43.Exchanger 了解吗?
Exchanger (交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

这两个线程通过 exchange 方法交换数据,如果第一个线程先执行exchange() 方法,它会一直等待第二个线程也执行exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
Exchanger 可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出2 个交配结果。Exchanger 也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用AB岗两人进行录入,录入到Excel 之后,系统需要加载这两个Excel ,并对两个Excel 数据进行校对,看看是否录入一致。
java
public class ExchangerTest {
private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);
public static void main(String[] args) {
threadPool.execute(new Runnable() {@ Override
java
public void run() {
try {StringA=" 银行流水A ";//A 录入银行流水数据
java
exgr.exchange(A);
} catch (InterruptedException e) {
}
}
});
threadPool.execute(new Runnable() {@ Override
java
public void run() {
try {StringB=" 银行流水B ";//B 录入银行流水数据
java
String A = exgr.exchange("B");
System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"
+ A + ",B录入是:" + B);
} catch (InterruptedException e) {
}
}
});
threadPool.shutdown();
}
}假如两个线程有一个没有执行exchange() 方法,则会一直等待,如果担心有特殊情况发生,避免一直
等待,可以使用exchange(V x, long timeOut, TimeUnit unit) 设置最大等待时长。