09-14条多线程和并发高质量准则

活着就是为了改变世界,难道还有其他原因吗? —— 史蒂夫 · 乔布斯

118:不推荐覆写start方法

119:启动线程前stop方法是不可靠的

120:不使用stop方法停止线程

1、stop方法是过时的:从Java编码规则来说,已经过时的方法不建议采用,弃了。

2、stop方法会导致代码逻辑不完整:stop方法是一种“恶意”的中断,一旦执行stop方法,即终止当前正在运行的线程,不管线程逻辑是否完整,这是非常危险的。

3、stop方法破坏原子逻辑

多线程为了解决共享资源抢占的问题,使用了锁概念,避免资源不同步,但是正因为如此,stop带了更大了麻烦,它会丢弃所有的锁,导致原子逻辑受损。

如何关闭线程呢?

1
2
3
if (!thread.isInterrupted()) {
thread.interrupt();
}

如果使用的是线程池,可以通过shutdown方法逐步关闭池中的线程。

121:线程优先级只使用三个等级

线程的优先级(Priority)决定了线程获取CPU运行的机会,优先级越高获取的运行机会越大,优先级月底获取的机会越小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestThread implements Runnable {
public void start(int _priority) {
Thread t = new Thread(this);
// 设置优先级别
t.setPriority(_priority);
t.start();
}
@Override
public void run() {
// 消耗CPU的计算
for (int i = 0; i < 100000; i++) {
Math.hypot(924526789, Math.cos(i));
}
// 输出线程优先级
System.out.println("Priority:" + Thread.currentThread().getPriority());
}

public static void main(String[] args) {
//启动20个不同优先级的线程
for (int i = 0; i < 20; i++) {
new TestThread().start(i % 10 + 1);
}
}
}

image-20210218170241756

创建了20个线程,优先级设置的不同,执行起来是这样的,5和6反了。

1、并不是严格按照线程优先级来执行的

因为优先级只是表示线程获取CPU运行的机会,并不是代码强制的排序号。

2、优先级差别越大,运行机会差别越明显

Java的缔造者们也觉察到了线程优先问题,于是Thread类中设置了三个优先级,此意就是告诉开发者,建议使用优先级常量,而不是1到10的随机数字。常量代码如下:

1
2
3
4
5
public class Thread implements Runnable {
public final static int MIN_PRIORITY = 1;
public final static int NORM_PRIORITY = 5;
public final static int MAX_PRIORITY = 10;
}

开发时只使用此三类优先级就可以了。

122:使用线程异常处理器提升系统可靠性

编写一个socket应用,监听指定端口,实现数据包的接收和发送逻辑,这在早起系统间进行数据交互是经常使用的,这类接口通常考虑两个问题:一个是避免线程阻塞,保证接收的数据尽快处理;二是接口的稳定性和可靠性,数据包很复杂,接口服务的系统也很多,一旦守候线程出现异常就会导致socket停止,这是非常危险的,那我们有什么办法避免呢?

Java1.5版本以后在thread类中增加了setUncaughtExceptionHandler方法,实现了线程异常的捕捉和处理。

代码实例:

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 TcpServer implements Runnable {
public TcpServer() {
Thread t = new Thread(this);
t.setUncaughtExceptionHandler(new TcpServerExceptionHandler());
t.start();
}

@Override
public void run() {
for (int i = 0; i < 3; i++) {
try{
Thread.sleep(1000);
System.out.println("系统正常运行:"+i);
}catch (InterruptedException e){
e.printStackTrace();
}
}
throw new RuntimeException();
}

private static class TcpServerExceptionHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("线程"+t.getName()+" 出现异常,自行重启,请分析原因。");
e.printStackTrace();
new TcpServer();
}
}

public static void main(String[] args) {
TcpServer tcpServer = new TcpServer();
}
}

这段代码的逻辑比较简单,在TcpServer类创建时启动一个线程,提供TCP服务,例如接收和发送文件,具体逻辑在run方法中实现。同时,设置了该线程出现运行期异常时,由TcpServerExceptionHandler异常处理器来处理异常。那么TcpServerExceptionHandler做什么呢?两件事:

1、记录异常信息,以便查找问题

2、重新启动一个新线程,提供不间断的服务

有了这两点,TcpServer就可以稳定的运行了,即使出现异常也能自动重启,客户代码比较简单,只需要new TcpServer()即可,运行结果如下:

image-20210218170252719

从运行结果可以看出,当Thread-0出现异常时,系统自动重启了Thread-1线程,继续提供服务,大大提高了系统的性能。

这段代码只是一个示例程序,若要在实际环境中应用,则需要注意以下三个方面:

1、共享资源锁定:如果线程产生异常的原因是资源被锁定,自动重启应用会增加系统的负担,无法提供不间断服务。例如一个即时通信服务出现信息不能写入的情况,即时再怎么重启服务,也无法解决问题。在此情况下最好的办法是停止所有的线程,释放资源。

2、脏数据引起系统逻辑混乱:异常的产生中断了正在执行的业务逻辑,特别是如果正在处理一个原子操作,但如果此时抛出了运行期异常就有可能会破坏正常的业务逻辑,例如出现用户认证通过了,但签到不成功的情况,在这种情况下重启应用程序,虽然可以提供服务,但对部分用户产生了逻辑异常。

3、内存溢出:线程异常了,但由该线程创建的对象并不会马上回收,如果再重新启动新线程,再创建一批对象,特别是加入了场景接管,就非常危险了,例如即时通信服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务重启,在这种情况下,就需要在线程初始化时加载大量对象以保证用户的状态信息,但是如果线程反复重启,很可能会引起OutOfMemory内存泄漏问题。

123:volatile不能保证数据同步

volatile关键字比较少用,原因无外乎两点,一是在Java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;而且比较难设计,误用较多,这也导致它的“名誉”受损。

我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,比如寄存器Register、高速缓存存储器Cache等),线程的计算一般是通过工作内存进行交互的,其示意图如下图所示:

image-20210218170302754

从示意图中我们可以看到,线程在初始化时从主内存中加载需要的变量值到工作内存中,然后在线程运行时,如果是读取,直接从工作内存中读取,如果是写入,则先写入工作内存中,之后刷新到主内存中,这是JVM的一个简单的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主内存,但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最新的值,此时就会出现不同线程持有的公共资源不同步的情况。

对于此问题有很多解决的办法,比如使用synchronized同步代码块,或者使用Lock锁来解决该问题,不过,Java可以使用volatile更简单的解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获取到最新的变量值,其示意图如下:

image-20210218170314384

明白了volatile变量的原理,那我们来思考一下:volatile变量是否能够保证数据的同步性呢?两个线程同时修改volatile变量是否会产生脏数据呢?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class UnsafeThread implements Runnable {
//共享资源
private volatile int count = 0;
@Override
public void run() {
// 增加CPU的繁忙程度,不必关心其逻辑含义
for (int i = 0; i < 1000; i++) {
Math.hypot(Math.pow(92456789,i),Math.cos(i));
}
count++;
}
public int getCount(){
return count;
}
}

上面的代码定义了一个多线程,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,确保是从内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加操作,count变量会产生脏数据吗?模拟多线程代码如下:

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
public static void main(String[] args) {
// 理想值,并作为最大循环次数
int value = 1000;
// 循环次数,防止造成无限循环或者死循环
int loops = 0;
// 主线程组,用于估计活动线程数
ThreadGroup tg = Thread.currentThread().getThreadGroup();
while (loops++<value){
// 共享资源清零
UnsafeThread ut = new UnsafeThread();
for (int i = 0; i < value; i++) {
new Thread(ut).start();
}
// 先等15毫秒,等待活动线程为1
do {
try {
Thread.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}while (tg.activeCount()!=1);
//检查实际值与理论值是否一致
if(ut.getCount()!=value){
//出现线程不安全的情况
System.out.println("循环到:"+loops+" 遍,出现线程不安全的情况");
System.out.println("此时,count= "+ut.getCount());
System.exit(0);
}
}
}

此段代码的逻辑如下:

1、启动1000个线程,修改共享资源count的值

2、暂停15毫秒,观察活动线程数是否为1(即只剩下主线程再运行),若不为1,则再等待15毫秒。

3、共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。

4、如果没有找到,继续循环,直到达到最大循环为止。

运行结果:

image-20210218170325104

执行完了,没出现不安全的情况,证明volatile性能还是可以的。

书中自有黄金屋,书中自有颜如玉!

书中的运行结果:

循环到:40遍,出现不安全的情况

此时,count=999

这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程的安全。

代码执行完毕,原本期望的结果为1000,但运行后的结果为999,这表示出现了线程不安全的情况。这也就说明了:volatile关键字只能保证当前线程需要该变量的值时能够获得最新的值,并不能保证线程修改的安全性。

顺便说一下,上面的代码中,UnsafeThread类消耗CPU计算时必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,否者很难模拟出volatile线程不安全的情况,大家可以实际测试一下。

UnsafeThread消耗CPU很严重,慎用啊。

image-20210218170334960

124:异步运算考虑使用Callable接口

多线程应用有两种实现方式,一种是实现runnable接口,另一种是继承Thread类,这两种方法都有缺点:run方法没有返回值,不能抛出异常(这两个缺点归根到底就是runnable接口的缺陷,Thread类也是实现了runnable接口),如果需要知道一个线程的运行结果就需要用户自行设计,线程类本身并不能提供返回值和异常。但是Java1.5引入了一个新的接口callable,它类似于runnable接口,实现它也可以实现多线程任务。

好不好测一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.*;

public class TaxCalculator implements Callable {
//本金
private int seedMoney;

//接收主线程提供的参数
public TaxCalculator(int _seedMoney){
seedMoney = _seedMoney;
}

@Override
public Integer call() throws Exception {
// 复杂计算,运行一次需要2秒
TimeUnit.MILLISECONDS.sleep(2000);
return seedMoney/10;
}
}

模拟一个复杂运算:税款计算器,该运算可能要花费10秒的时间,用户此时一直等啊等,很烦躁,需要给点提示,让用户知道程序在运行,没卡死。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws InterruptedException, ExecutionException {
//生成一个单线程的异步执行器
ExecutorService es = Executors.newSingleThreadExecutor();
//线程执行后的期望值
Future<Integer> future = es.submit(new TaxCalculator(100));
while (!future.isDone()){
// 还没有运算完成,等待50毫秒
TimeUnit.MILLISECONDS.sleep(50);
System.out.print("*");
}
System.out.println("\n计算完成,税金是:"+future.get()+" 元");
es.shutdown();
}

Executors是一个静态工具类,提供了异步执行器的创建能力,如单线程异步执行器newSingleThreadExecutor、固定线程数量的执行器newFixedThreadPool等,一般它是异步计算的入口类。future关注的是线程执行后的结果,比如运行十分完毕,结果是多少等。

image-20210218170347826

执行时,”*”会依次递增,表示系统正在运算,为用户提供了运算进度,此类异步计算的好处是:

1、尽可能多的占用系统资源,提高运算速度

2、可以监控线程的执行情况。比如执行是否完毕、是否有返回值、是否有异常等。

3、可以为用户提供更好的支持,比如例子中的运算进度等。

125:优先选择线程池

126:适时选择不同的线程池来实现

Java线程池原理及实现

127:lock与synchronized是不一样的

直接上代码:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.Calendar;

public class Task {
public void doSomething() {
try {
// 每个线程等待2秒钟,注意此时线程的状态转变为Warning状态
Thread.sleep(2000);
} catch (Exception e) {
// 异常处理
}
StringBuffer sb = new StringBuffer();
// 线程名称
sb.append("线程名称:" + Thread.currentThread().getName());
// 运行时间戳
sb.append(",执行时间: " + Calendar.getInstance().get(Calendar.SECOND) + "s");
System.out.println(sb);
}
}

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TaskWithLock extends Task implements Runnable{
// 声明显示锁
private final Lock lock = new ReentrantLock();
@Override
public void run() {
try {
// 开始锁定
lock.lock();
doSomething();
} finally {
// 释放锁
lock.unlock();
}
}
}

public class TaskWithSync extends Task implements Runnable{
@Override
public void run() {
synchronized ("A"){
doSomething();
}
}
}

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Client {
public static void runTasks(Class<? extends Runnable> clz) throws Exception{
ExecutorService es = Executors.newCachedThreadPool();
System.out.println("***开始执行 " + clz.getSimpleName() + " 任务***");
// 启动3个线程
for (int i = 0; i < 3; i++) {
es.submit(clz.newInstance());
}
// 等待足够长的时间,然后关闭执行器
TimeUnit.SECONDS.sleep(10);
System.out.println("---" + clz.getSimpleName() + " 任务执行完毕---\n");
// 关闭执行器
es.shutdown();
}

public static void main(String[] args) throws Exception{
// 运行显示任务
runTasks(TaskWithLock.class);
// 运行内部锁任务
runTasks(TaskWithSync.class);
}
}

image-20210218170505192

显示锁是同时运行的,很显然pool-1-thread-1线程执行到sleep时,其它两个线程也会运行到这里,一起等待,然后一起输出,这还具有线程互斥的概念吗?

而内部锁的输出则是我们预期的结果,pool-2-thread-1线程在运行时其它线程处于等待状态,pool-2-threda-1执行完毕后,JVM从等待线程池中随机获的一个线程pool-2-thread-2执行,最后执行pool-2-thread-3,这正是我们希望的。

现在问题来了:Lock锁为什么不出现互斥情况呢?

这是因为对于同步资源来说显示锁是对象级别的锁,内部锁是类级别的锁,lock定义为多线程类的私有属性是起不到互斥作用的,除非把lock定义为所有线程的共享变量。

改一下代码,将lock定义在测试类中

1
2
// 声明显示锁
public static final Lock lock = new ReentrantLock();

image-20210218170533381

除了这一点不同之外,显示锁和内部锁还有什么区别呢?还有以下4点不同:

1、Lock支持更细精度的锁控制:

假设读写锁分离,写操作时不允许有读写操作存在,而读操作时读写可以并发执行,这一点内部锁就很难实现。显示锁的示例代码如下: 

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
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class Foo {
// 可重入的读写锁
private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
private static final Lock r = rwl.readLock();
// 写锁
private static final Lock w = rwl.writeLock();

// 多操作,可并发执行
public static void read() {
try {
r.lock();
Thread.sleep(1000);
System.out.println("read......");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
}

// 写操作,同时只允许一个写操作
public static void write() {
try {
w.lock();
Thread.sleep(1000);
System.out.println("write.....");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
}
}

可以编写一个Runnable实现类,把Foo类作为资源进行调用(注意多线程是共享这个资源的),然后就会发现这样的现象:读写锁允许同时有多个读操作但只允许一个写操作,也就是当有一个写线程在执行时,所有的读线程都会阻塞,直到写线程释放锁资源为止,而读锁则可以有多个线程同时执行。

2、Lock锁是无阻塞的,synchronized是阻塞的

3、Lock可实现公平锁,synchronized只能是非公平锁

4、Lock是代码级的,synchronized是JVM级的

Lock是通过编码实现的,synchronized是在运行期由JVM释放的,相对来说synchronized的优化可能性高,毕竟是在最核心的部分支持的,Lock的优化需要用户自行考虑。

显示锁和内部锁的功能各不相同,在性能上也稍有差别,但随着JDK的不断推进,相对来说,显示锁使用起来更加便利和强大,在实际开发中选择哪种类型的锁就需要根据实际情况考虑了:灵活、强大选择lock,快捷、安全选择synchronized。

128:预防线程死锁

1、死锁的概念

死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局。当进程处于僵持状态时,若无外力作用,它们都将无法再向前推进。

2、产生死锁的原因

① 竞争资源

可剥夺资源和非剥夺性资源:

进程在获得这类资源后,该资源可以再被其它线程剥夺,CPU和主存均属于可剥夺性资源。另一类资源是不可剥夺性资源,当系统把这类资源分配给某进程后,再不能强行回收,只能在进程用完后自行释放,如磁带机、打印机等。

竞争非剥夺性资源:

在系统中所配置的非剥夺性资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷入僵局。

竞争临时资源:

临时资源是指由一个进程产生,被另一个进程使用短暂时间后变无用的资源,它也可能产生死锁。

② 进程间推进顺序非法

进程在运行过程中,请求和释放资源的顺序不当,同样会产生死锁。

3、死锁的一些常用概念:

① 互斥条件:

指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求该资源,则请求者只能等待,直至占有该资源的进程用毕释放。

② 请求和保持条件:

指进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其它进程占有,此时请求进程阻塞,但又对自己获得的其它资源保持不放。

③ 不剥夺资源:

指进程已获得资源,在使用完之前,不能被剥夺,只能在使用完时由自己释放。

④ 环路等待条件:

指在发生死锁时,必然存在一个进程—资源的环形链,即进程集合(P0,P1,P2,…,Pn)中的P0正在等待一个P1占用的资源;P1正在等待一个P2占用的资源,……,Pn正在等待已被P0占用的资源。

129:适当设置阻塞队列的长度

ArrayBlockingQueue类最常用的add方法

如果直接调用offer方法插入元素, 在超出容量的情况下, 它除了返回false外, 不会提供任何其他信息, 如果代码不做插入判断, 那就会造成数据的“默默”丢失, 这就是它与非阻塞队列的不同之处。

如果应用期望无论等待多长时间都要运行该任务, 不希望返回异常就需要用BlockingQueue接口定义的put方法了, 它的作用也是把元素加入到队列中, 但它与add、offer方法不同, 它会等待队列空出元素, 再让自己加入进去, 通俗地讲, put方法提供的是一种“无赖”式的插入, 无论等待多长时间都要把该元素插入到队列中。
与插入元素相对应, 取出元素也有不同的实现, 例如remove、poll、take等方法, 对于此类方法的理解要建立在阻塞队列的长度固定的基础上, 然后根据是否阻塞、阻塞是否超时等实际情况选用不同的插入和提取方法。

130:使用CountDownLatch协调子线程

CountDownLatch是一个非常实用的多线程控制工具类。常用的就下面几个方法:

1
2
3
CountDownLatch(int count) //实例化一个倒计数器,count指定计数个数
countDown() // 计数减一
await() //等待,当计数减到0时,所有线程并行执行

对于倒计数器,一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检测。只有等到所有的检查完毕后,引擎才能点火。那么在检测环节当然是多个检测项可以同时进行的。代码实现:

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
public class CountDownLatchDemo implements Runnable{
static final CountDownLatch latch = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();

@Override
public void run() {
// 模拟检查任务
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("check complete");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//计数减一
//放在finally避免任务执行过程出现异常,导致countDown()不能被执行
latch.countDown();
}
}


public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i=0; i<10; i++){
exec.submit(demo);
}

// 等待检查
latch.await();

// 发射火箭
System.out.println("Fire!");
// 关闭线程池
exec.shutdown();
}
}

上述代码中我们先生成了一个CountDownLatch实例。计数数量为10,这表示需要有10个线程来完成任务,等待在CountDownLatch上的线程才能继续执行。latch.countDown();方法作用是通知CountDownLatch有一个线程已经准备完毕,倒计数器可以减一了。latch.await()方法要求主线程等待所有10个检查任务全部准备好才一起并行执行。

131:CyclicBarrier 让多线程齐步走

CyclicBarrier中文意思是“循环栅栏”

1、构造函数:

1
2
public CyclicBarrier(int parties)//parties 是参与线程的个数
public CyclicBarrier(int parties, Runnable barrierAction)//barrierAction是最后一个到达线程要做的任务

2、重要方法:

1
2
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
  • 线程调用 await() 表示自己已经到达栅栏
  • BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时

一个线程组的线程需要等待所有线程完成任务后再继续执行下一次任务,代码实例:

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
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierDemo {
static class TaskThread extends Thread {

CyclicBarrier barrier;

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

@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(getName() + " 到达栅栏 A");
barrier.await();
System.out.println(getName() + " 冲破栅栏 A");

Thread.sleep(2000);
System.out.println(getName() + " 到达栅栏 B");
barrier.await();
System.out.println(getName() + " 冲破栅栏 B");
} catch (Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
int threadNum = 5;
CyclicBarrier barrier = new CyclicBarrier(threadNum, new Runnable() {

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 完成最后任务");
}
});

for(int i = 0; i < threadNum; i++) {
new TaskThread(barrier).start();
}
}
}

image-20210218170556607

从打印结果可以看出,所有线程会等待全部线程到达栅栏之后才会继续执行,并且最后到达的线程会完成 Runnable 的任务。

3、CyclicBarrier 使用场景

可以用于多线程计算数据,最后合并计算结果的场景。

4、CyclicBarrier 与 CountDownLatch 区别

① CyclicBarrier 是可循环利用的,CountDownLatch 是一次性的

② CyclicBarrier 参与的线程职责是一样的,CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。


09-14条多线程和并发高质量准则
https://janycode.github.io/2016/12/28/02_编程语言/01_Java/05_高质量代码/09-14条多线程和并发高质量准则/
作者
Jerry(姜源)
发布于
2016年12月28日
许可协议