【JAVA进化论】LV5-5:线程调度:join、yield、sleep、interrupt

一、简介

这些方法属于线程对象里的方法,属于线程本身的操作。

join:用于等待一个线程的终止,等待期间将会进入阻塞状态,直到被等待的线程终止结束。

yield:用于线程让步,触发了此方法的线程会进入就绪状态,也就是说会让出CPU的调度一下,让CPU转去其他线程。

sleep:强制当前正在运行的线程进入阻塞状态,直到休眠期结束,才会再次进入运行状态。

interrupt:终止当前正在运行的线程。

二、用法

2.1:join

前面说过它简单的用法,来看个例子:

代码块1
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 static void main(String[] args) throws Exception {

System.out.println("main线程开始运行");

Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000L); //让当前线程阻塞1s
System.out.println("线程1运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

Thread t2 = new Thread(() -> {
try {
Thread.sleep(5000L); //让当前线程阻塞5s
System.out.println("线程2运行结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

//线程1和2启动
t1.start();
t2.start();

//利用join,可以让main线程必须等到它们俩线程运行完毕后才会继续往下执行
t1.join();
t2.join();

System.out.println("main线程运行结束");

}

运行结果:

图1

最终运行结果告诉我们,由于t1t2main线程join,因此main线程在join处必须要等到t1和t2都运行结束后才会继续往下运行。

它的流程像是下面这样:

图2

此时main方法总耗时约等于5s。

基于这种流程理解,我们让t1和t2调用join前,让main线程阻塞6s(超过t1和t2并发运行的总耗时),此时join便不再阻塞main线程了,因为t1和t2已经执行结束了:

图3

相比图2,main线程阻塞的时间甚至还多出了一截,此时总运行时间约为6s。

2.2:【实例】利用join优化一个大首页接口的效率

假设现在有一个网站,首页有顶部Banner位、左边栏、右边栏、用户信息几大模块需要加载,现在出一个接口,要求包装并吐出这几大模块的内容。

先来抽象一个首页接口对象:

代码块2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WebModule {

private String top; //顶部Banner位

private String left; //左边栏

private String right; //右边栏

private String user; //用户信息

//...get...set...

@Override
public String toString() {
return String.format("top: %s; left: %s; right: %s; user: %s", top, left, right, user);
}
}

定义我们的dao层,我们利用sleep来模拟实际的方法耗时:

代码块3
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
public class ModuleDao {

public String getTop() { // 这里假设getTop需要执行200ms
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "顶部banner位";
}

public String getLeft() { // 这里假设getLeft需要执行50ms
try {
Thread.sleep(50L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "左边栏";
}

public String getRight() { // 这里假设getRight需要执行80ms
try {
Thread.sleep(80L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "右边栏";
}

public String getUser() { // 这里假设getUser需要执行100ms
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "用户信息";
}

}

我们再定义一个service层,提供两个方法,它们的目的都是通过dao层提供的数据,封装成WebModule对象返回给网关层(controller层):

代码块4
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
public class ModuleService {

private ModuleDao moduleDao = new ModuleDao();

// 同步获取
public WebModule getWebModuleMsgSync() {
WebModule webModule = new WebModule();
webModule.setTop(moduleDao.getTop());
webModule.setLeft(moduleDao.getLeft());
webModule.setRight(moduleDao.getRight());
webModule.setUser(moduleDao.getUser());
return webModule;
}

// 同步获取,通过多线程优化性能
public WebModule getWebModuleMsgSimpleAsync() throws InterruptedException {

WebModule webModule = new WebModule();

Thread topTask = new Thread(() -> webModule.setTop(moduleDao.getTop()));
Thread leftTask = new Thread(() -> webModule.setLeft(moduleDao.getLeft()));
Thread rightTask = new Thread(() -> webModule.setRight(moduleDao.getRight()));
Thread userTask = new Thread(() -> webModule.setUser(moduleDao.getUser()));

//触发各个异步任务
topTask.start();
leftTask.start();
rightTask.start();
userTask.start();

//等待所有的任务均执行完毕
topTask.join();
leftTask.join();
rightTask.join();
userTask.join();

return webModule;
}

}

我们用controller层来测试下:

代码块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
//只是模拟,其实网关层应该是servlet或者spring boot的controller(后续会认识到)
public class ModuleController {

private ModuleService moduleService = new ModuleService();

public WebModule getWebModuleMsgSync() {
return moduleService.getWebModuleMsgSync();
}

public WebModule getWebModuleMsgSimpleAsync() throws InterruptedException {
return moduleService.getWebModuleMsgSimpleAsync();
}

public static void main(String[] args) throws Exception {

ModuleController moduleController = new ModuleController();

long start = System.currentTimeMillis(); //获取系统当前时间戳,ms
moduleController.getWebModuleMsgSync(); //同步获取各个模块的方法
System.out.println("同步获取各个模块,所需时间为:" + (System.currentTimeMillis() - start) + "ms");

long start2 = System.currentTimeMillis();
moduleController.getWebModuleMsgSimpleAsync(); //异步获取各个模块的方法
System.out.println("异步获取各个模块,所需时间为:" + (System.currentTimeMillis() - start2) + "ms");
}
}

运行结果如下:

代码块6
1
2
同步获取各个模块,所需时间为:442ms
异步获取各个模块,所需时间为:207ms

我们利用多线程异步+join的方式,将代码性能优化了足足1倍多。

我们再使用执行流程图来说明下它们的运行流程:

同步方法getWebModuleMsgSync的运行流程

图4

异步方法getWebModuleMsgSimpleAsync的运行流程

图5

请结合代码和流程图,仔细理解下join。截止到目前,你已经会用多线程优化一个慢接口了~

2.3:sleep

我们前面的例子在不断的用这个方法,只需要知道,你在任何地方写上Thread.sleep(xxx);这段代码,就是在让运行该段代码的线程阻塞(休眠)。

2.4:interrupt&stop

都用于主动终止一个线程,区别在于,interrupt比较温和stop属于暴力关停,我们下面通过一个例子来看下它们的区别。

线程本身调用这俩方法后,视为已终止状态,下面来用interrupt和join来做个实验:

代码块7
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
public class JoinTest {

private boolean isStop = false;

public static void main(String[] args) throws Exception {
JoinTest test = new JoinTest();
Thread loopT = new Thread(test::loopTask); //test::loopTask等于test.loopTask()
loopT.start();

sleep(2000L); //2s后终止线程
test.setStop(true);

long s = System.currentTimeMillis();
loopT.join();
System.out.println("线程终止后,join阻塞时间为:" + (System.currentTimeMillis() - s));
System.out.println("end~");
}

public void setStop(boolean stop) {
isStop = stop;
}

public void loopTask() {
while (!isStop) { //若状态为false,则继续执行下面的逻辑,每隔1s打印一次
sleep(1000L);
System.out.println("loop trigger ~");
}
Thread.currentThread().interrupt(); //在这里终止掉当前线程
//事实上,在终止掉线程后,还有接下来的逻辑要执行
long s = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
int[] a = new int[100]; //模拟耗时操作,这里不能用sleep了,因为当前线程已经被终止了
}
System.out.println("线程终止后,逻辑块运行时间:" + (System.currentTimeMillis() - s));
}

public static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

}

运行结果如下:

代码块8
1
2
3
4
5
loop trigger ~
loop trigger ~
线程终止后,逻辑块运行时间:98
线程终止后,join阻塞时间为:114
end~

通过interrupt终止线程,即便线程被终止了,后面的逻辑也会触发,join依旧会选择阻塞,直到后续逻辑执行完毕,事实上,大部分任务都可以及时的终止,比如第一个例子,异步出去的任务,最终都会执行完成,线程变为终止状态,join都可以顺利结束,但是反观上例,如果没人及时的设置isStop的值,程序会一直执行下去,没有终止态,join会无止境的终止下去。

而stop,线程的stop方法已被官方标记为“不建议使用”的方法,如果把上例的interrupt的调用换成stop,来看看其运行结果:

代码块9
1
2
3
4
loop trigger ~
loop trigger ~
线程终止后,join阻塞时间为:0
end~

可以看到,线程终止后的后续逻辑均没有触发,前面说过,stop是一种很粗暴的终止线程的方式,一旦被stop,那么里面的业务逻辑将直接断掉,因此官方并不推荐使用该方法来终止线程。

而interrupt,仅仅是对目标线程发送了了一个中断信号(改变了线程的中断状态而已),当目标线程再次通过obj.wait、thread.sleep、thread.join方法进入阻塞状态时,接收到该信号,就会抛出InterruptedException异常,这时候需要业务方自行处理或者直接抛出,以结束线程阻塞状态(这里需要注意的是被obj.wait方法阻塞时,抛出该异常需要目标线程再次获得实例对象obj的锁才行)。

上述三个“需要花费时间”的方法均抛出了InterruptedException异常,针对这些特性,想要完成以下操作就非常方便了:

  1. 取消wait方法等待notify/notifyAll的处理
  2. 取消在sleep方法指定时间内停止的处理
  3. 取消join方法等待其他线程终止的处理

取消之后所做的处理,取决于需求,可能会终止线程,或者通知用户已取消,或者终止当前处理进入下一个处理阶段。

2.5:yield

通过前面的理解,我们知道线程是靠CPU通过来回切换执行的方式来执行多个线程的,那么你知道你的线程通过代码调用start后,会发生哪些状态变化吗?

首先,start触发线程后,线程状态进入运行状态,而我们前面讲过CPU是来回切换运行线程的,所以我们针对”被调度到“和”等待被调度“两种情况又对运行状态又做了细分,即运行中和就绪:

⚠️ 请务必仔细跟着图中的序号顺序梳理一遍这个流程,对之后的线程生命周期理解起来很有帮助

图6

为什么要说这个呢?因为它可以帮你理解yield,yield被当前线程触发后,首先当前线程会直接进入”就绪状态“,你可以这么理解,当前线程做出yield调用之后,会将本该调度到它的CPU”让出去“,让CPU重新调度一次,注意我这里说的是重新调度一次,所以即便调用了yield,下次CPU仍然有概率调度到它,来看下这个过程:

图7

看到了吗?任意线程只要自行触发自己的yield方法,就会立马让自己变成就绪态,然后让CPU重新选择线程调度,重新调度意味着可能会调度到其它线程,也可能再次调度到自己,如果再次调度到自己(如图中的情况2),那么就只能继续往下执行咯,等于yield无用,如果调度到别人(如图中的情况1)则yield有意义,因为你确实让出了CPU让别人进入运行态了。

就不举例子了,你也看到了,yield的意义是这样的,本就不好举例,但是这个过程一定要梳理清楚,至少你要知道线程处于运行状态的时候,又根据CPU调度状态细分了就绪和运行中状态。

三、线程状态迁移图

截止目前,我们知道一个线程对象可以通过下面的方法改变自己的运行状态:

  1. 通过start让自己变成运行中状态。而运行中状态又根据有没有被CPU调度到分成了就绪态和运行中两个状态
  2. 通过sleep让自己陷入阻塞状态,阻塞状态意味着CPU不会再调度到它,除非它解除阻塞变为就绪状态
  3. 通过调用其它线程的join方法让自己在其它线程结束前陷入阻塞状态,其它线程结束后,自己再次由阻塞变为就绪状态
  4. 通过某个对象的wait方法,让自己处于阻塞状态,然后再次通过同一对象的notify触发,让自己变成就绪状态
  5. 通过yield让自己让出正在调度自己的CPU,让自己直接进入就绪状态
  6. 通过interrupt或者stop让自己变成终止状态

我们结合上面的六条信息,整理下一个线程的生命周期,应该包含哪几个状态:

运行状态(又细分为就绪和运行中两个小状态)、阻塞状态、终止状态

让我们来画下线程的生命周期,或者叫线程状态转换图:

图8