线程与进程


总体路线

img

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器

二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享

  • 进程间通信较为复杂

    • 同一台计算机的进程通信称为 IPC(Inter-process communication)
    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

并行与并发

并发

单核 cpu 下,线程实际还是串行执行的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent

CPU 时间片1 时间片2 时间片3 时间片4
core 线程1 线程2 线程3 线程4

image-20220813150625546

并行

多核 cpu下,每个核(core) 都可以调度运行线程,这时候线程可以是并行的,不同的线程同时使用不同的cpu在执行。

CPU 时间片1 时间片2 时间片3 时间片4
core1 线程1 线程1 线程3 线程3
core2 线程2 线程4 线程2 线程4

image-20220813150754707

引用 Rob Pike 的一段描述

  • 并发(concurrent)是同一时间应对(dealing with)多件事情的能力
  • 并行(parallel)是同一时间动手做(doing)多件事情的能力

应用

异步调用

调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步
  • 不需要等待结果返回,就能继续运行就是异步

设计

多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…

1
2
3
4
5
6
7
8
//同步
@Slf4j(topic = "c.Sync")
public class Sync {
public static void main(String[] args) {
FileReader.read(Constants.MP4_FULL_PATH);
log.debug("do other things ...");
}
}

输出结果:

1
2
3
17:23:56.401 c.FileReader [main] - read [015-hd1080.pro-german-korb-5643935.mp4] start ...
17:23:56.503 c.FileReader [main] - read [015-hd1080.pro-german-korb-5643935.mp4] end ... cost: 103 ms
17:23:56.503 c.Sync [main] - do other things ...
1
2
3
4
5
6
7
8
//异步
@Slf4j(topic = "c.Async")
public class Async {
public static void main(String[] args) {
new Thread(() -> FileReader.read(Constants.MP4_FULL_PATH)).start();
log.debug("do other things ...");
}
}

输出结果:

1
2
3
17:25:40.498 c.Async [main] - do other things ...
17:25:40.498 c.FileReader [Thread-0] - read [015-hd1080.pro-german-korb-5643935.mp4] start ...
17:25:40.608 c.FileReader [Thread-0] - read [015-hd1080.pro-german-korb-5643935.mp4] end ... cost: 111 ms

结论

  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
  • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

提高效率

充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。

计算 1 花费 10 ms

计算 2 花费 11 ms

计算 3 花费 9 ms

汇总需要 1 ms

  • 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
  • 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms

注意

需要在多核 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
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
@Fork(1)
@BenchmarkMode(Mode.AverageTime)//测试模式:统计平均时间
@Warmup(iterations=3)//热身次数:3次
@Measurement(iterations=5)//5轮测试
public class MyBenchmark {
static int[] ARRAY = new int[1000_000_00];
static {
Arrays.fill(ARRAY, 1);
}

/**
* 求1亿个数相加的值
* 四个线程,每个线程分别为两千五百个数相加,最后求和
* @return
* @throws Exception
*/
@Benchmark
public int c() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[0+i];
}
return sum;
});
FutureTask<Integer> t2 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[250_000_00+i];
}
return sum;
});
FutureTask<Integer> t3 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[500_000_00+i];
}
return sum;
});
FutureTask<Integer> t4 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 250_000_00;i++) {
sum += array[750_000_00+i];
}
return sum;
});
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
new Thread(t4).start();
return t1.get() + t2.get() + t3.get()+ t4.get();
}

/**
* 一个线程计算一亿个数相加
* @return
* @throws Exception
*/
@Benchmark
public int d() throws Exception {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(()->{
int sum = 0;
for(int i = 0; i < 1000_000_00;i++) {
sum += array[0+i];
}
return sum;
});
new Thread(t1).start();
return t1.get();
}
}

在多核CPU上的输出结果:

1
2
3
Benchmark            Mode  Samples  Score  Score error  Units
c.i.MyBenchmark.c avgt 5 0.016 0.002 s/op
c.i.MyBenchmark.d avgt 5 0.038 0.009 s/op

在单核CPU上的输出结果:

1
2
3
Benchmark            Mode  Samples  Score  Score error  Units
c.i.MyBenchmark.c avgt 5 0.041 0.002 s/op
c.i.MyBenchmark.d avgt 5 0.038 0.009 s/op

结论

  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的

    • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
    • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

IO

IO即Input/Output 输入输出,诺依曼结构将计算机分成5个基础部分:运算器、控制器、存储器、输入、输出

img

应用程序发起一次 IO 操作,包含两个阶段:

  1. IO 调用:应用程序进程向操作系统内核发起调用。
  2. IO 执行:操作系统内核完成 IO 操作。

IO 又分成 BIO(阻塞 IO)、NIO(非阻塞 IO)。

  • 阻塞IO非阻塞IO

这两个概念是程序级别的。主要描述的是程序请求操作系统 IO 操作后,如果 IO 资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有 IO 资源准备好了)

  • 同步IO非同步IO

这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求 IO 操作后,如果 IO 资源没有准备好,该如何响应程序的问题: 前者不响应,直到 IO 资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当 IO 资源准备好以后,再用事件机制返回给程序

Java 线程

创建和运行线程

直接使用 Thread

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
//创建线程对象
Thread t = new Thread(){
//重写run方法
@Override
public void run() {
log.debug("running");
}
};
//给线程起名
t.setName("t1");
//启动线程
t.start();
=================================
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();
================================
//lambda表达式
Thread t2 = new Thread(()->{ log.debug("running"); }, "t2");
t2.start();

使用 Runnable 配合 Thread

把【线程】和【任务】(要执行的代码)分开

  • Thread 代表线程
  • Runnable 可运行的任务(线程要执行的代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t1 = new Thread(runnable, "t1");
// 启动线程
t1.start();
================================
//lambda表达式
// 创建任务对象
Runnable task2 = ()->log.debug("hello");
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();

原理之 Thread 与 Runnable 的关系

  • Thread 是一个类,而 Runnable 是一个接口。
  • Thread 类实现了 Runnable 接口,Runnable 接口里只有一个抽象的 run() 方法。说明 Runnable 不具备多线程的特性。Runnable 依赖 Thread 类的 start 方法创建一个子线程,再在这个子线程里调用 run() 方法,才能让 Runnable 接口具备多线程的特性

小结

  • 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
  • 用 Runnable 更容易与线程池等高级 API 配合
  • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况,Future 就是对于具体的 Runnable 或者 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果

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
// 创建任务对象,匿名内部类
FutureTask<Integer> task3 = new FutureTask<>(() -> {
log.debug("hello");
return 100;
});
// 参数1 是任务对象; 参数2 是线程名字,推荐
new Thread(task3, "t3").start();
// 主线程阻塞,同步等待 task 执行完毕的结果
Integer result = task3.get();
log.debug("结果是:{}", result);
=====================================
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 实现多线程的第三种方法可以返回数据
FutureTask futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("多线程任务");
Thread.sleep(100);
return 100;
}
});
// 主线程阻塞,同步等待 task 执行完毕的结果
new Thread(futureTask,"我的名字").start();
log.debug("主线程");
log.debug("{}",futureTask.get());
}
1
2
3
4
5
6
7
8
9
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}

观察多个线程同时运行

主要是理解

  • 交替执行
  • 谁先谁后,不由我们控制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestMultiThread")
public class TestMultiThread {

public static void main(String[] args) {
new Thread(() -> {
while(true) {
log.debug("running");
}
},"t1").start();
new Thread(() -> {
while(true) {
log.debug("running");
}
},"t2").start();
}
}

查看进程线程的方法

windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程
  • tasklist 查看进程
  • taskkill 杀死进程

linux

  • ps -fe 查看所有进程
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill 杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID> 查看某个进程(PID)的所有线程

Java

  • jps 命令查看所有 Java 进程
  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态
  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

需要以如下方式运行 java 类

1
2
3
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote 
-Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接
-Dcom.sun.management.jmxremote.authenticate=是否认证 java类
  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 复制 jmxremote.password 文件
  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
  • 连接时填入 controlRole(用户名),R&D(密码)

原理之线程运行

栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的。

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
  • Context Switch 频繁发生会影响性能

常见方法

方法名 static 功能 注意
start() 启动一个新线程,在新的线程运行run方法中的代码 start方法只是让线程进入就绪,里面的代码不一定立刻运行(CUP的时间片还没有分给他)。每个线程对象的start方法只能调用一次,如果调用多次会出现IllegalThreadStateException
run() 新线程启用后会调用的方法 如果在构造Thread对象时传递了Runnable参数,则线程启动后调用Runnable中的run方法,否则默认不执行任何操作。但可以穿件Thread的子类对象,来覆盖默认行为
join() 等待线程运行结束
join(long n) 等待线程运行结束,最多等待n毫秒
getId() 获取线程长整型的id id唯一
getName() 获取线程名
setName(String) 修改线程名
getPriority() 获取线程优先级
setPriority(int) 修改线程优先级 java中规定优先级是1~10的整数,比较大优先级能提高该线程被CPU调用的几率
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
isInterrupted() 判断是否被打断 不会清除"打断标记",如果该线程已经中断,则返回 true;否则返回 false。
isAlive() 线程是否存活 (还没有运行完 毕)
interrupt() 打断线程,中断一个线程(设置标志位) 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除打断标记 ;如果打断的正在运行的线程,则会设置打断标记,线程并不会暂停;park 的线程被打断,也会设置打断标记
interrupted() static 判断当前线程是否被打断 会清除打断标记,如果当前线程已经中断,则返回 true;否则返回 false。
currentThread() static 获取当前正在执行的线程
sleep(long n) static 让当前执行的线程休眠n毫秒, 休眠时让出 cpu 的时间片给其它 线程
yield() static 提示线程调度器让出当前线程对 CPU的使用 主要是为了测试和调试

start 与 run

调用 start(能不能运行任务调度器说了算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test{
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run(){
log.debug("我是一个新建的线程正在运行中");
FileReader.read(Constants.MP4_FULL_PATH);
}
};
thread.setName("新建线程");
thread.start();
log.debug("主线程");
}
}

输出:程序在 t1 线程运行, run()方法里面内容的调用是异步的 Test.java

1
2
3
4
11:59:40.711 [main] DEBUG com.concurrent.test.Test - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test - 我是一个新建的线程正在运行中
11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms

调用 run

将上面代码的thread.start();改为 thread.run();输出结果如下:程序仍在 main 线程运行, run()方法里面内容的调用还是同步

1
2
3
4
12:03:46.711 [main] DEBUG com.concurrent.test.Test - 我是一个新建的线程正在运行中
12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
12:03:46.730 [main] DEBUG com.concurrent.test.Test - 主线程

小结

  • 直接调用 run() 是在主线程中执行了 run(),没有启动新的线程
  • 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法 中的代码
  • 当调用 start 方法后,线程状态会由“NEW”变为“RUNABLE”,此时再次调用 start 方法会报错 IllegalThreadStateException(非法的状态异常)

sleep 与 yield

sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞
  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,那么被打断的线程这时就会抛出 InterruptedException异常【注意:这里打断的是正在休眠的线程,而不是其它状态的线程】
  3. 睡眠结束后的线程未必会立刻得到执行(需要分配到cpu时间片)
  4. 建议用 TimeUnit 的 sleep() 代替 Thread 的 sleep()来获得更好的可读性

yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
  2. 具体的实现依赖于操作系统的任务调度器(就是可能没有其它的线程正在执行,虽然调用了yield方法,但是也没有用)

小结

yield 使 cpu 调用其它线程,但是 cpu 可能会再分配时间片给该线程;而 sleep 需要等过了休眠时间之后才有可能被分配 cpu 时间片

防止 CPU 占用 100%

在没有利用 CPU 来计算时,不要让 while(true) 空转浪费 CPU,这时可以使用 yield 或 sleep 来让出 CPU 的使用权给其他程序

1
2
3
4
5
6
7
while(true){
try{
Thread.sleep(50);
} catch(InterruptedException e){
e.printStackTrace();
}
}
  • 可以用 wait 或 条件变量达到类似的效果
  • 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
  • sleep 适用于无需锁同步的场景

线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

join

等待线程运行结束

应用之同步:在主线程中调用 t1.join,则主线程会等待 t1 线程执行完之后再继续执行 Test.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test{
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}

1583483843354

等待多个线程结果

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
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException {
test();
}

private static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(1);
r1 = 10;
});
Thread t2 = new Thread(() -> {
sleep(2);
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}

输出结果:用时两秒,颠倒 join 加入时机,用时不变

1
2
3
4
21:57:38.013 c.TestJoin [main] - join begin
21:57:40.035 c.TestJoin [main] - t2 join end
21:57:40.035 c.TestJoin [main] - t1 join end
21:57:40.035 c.TestJoin [main] - r1: 10 r2: 20 cost: 2023

image-20220813220047995

有时效的join

join(long n):等待线程运行结束,最多等待n毫秒

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
@Slf4j(topic = "c.TestJoin")
public class TestJoin {
static int r = 0;
static int r1 = 0;
static int r2 = 0;

public static void main(String[] args) throws InterruptedException {
test2();
}

public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
sleep(2);
r1 = 10;
});

long start = System.currentTimeMillis();
t1.start();

// 线程执行结束会导致 join 结束
log.debug("join begin");
t1.join(3000);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
}

输出结果:t1.join(1500);

1
2
22:05:30.209 c.TestJoin [main] - join begin
22:05:31.723 c.TestJoin [main] - r1: 0 r2: 0 cost: 1516

t1.join(3000):当线程运行结束时,join 设置的时效则失效,以实际线程运行时间为准

1
2
22:09:12.349 c.TestJoin [main] - join begin
22:09:14.359 c.TestJoin [main] - r1: 10 r2: 0 cost: 2012

interrupt 方法详解

打断 sleep,wait,join 的线程

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,

打断 sleep 的线程,会清空打断标记,以 sleep 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
log.debug("线程任务执行");
try {
Thread.sleep(10000); // wait, join
} catch (InterruptedException e) {
//e.printStackTrace();
log.debug("被打断");
}
}
};
t1.start();
Thread.sleep(500);
log.debug("111是否被打断?{}",t1.isInterrupted());
t1.interrupt();
log.debug("222是否被打断?{}",t1.isInterrupted());
Thread.sleep(500);
log.debug("222是否被打断?{}",t1.isInterrupted());
log.debug("主线程");
}

输出结果:(我下面将中断和打断两个词混用)可以看到,打断 sleep 的线程, 会清空中断状态,刚被中断完之后 t1.isInterrupted()的值为true,后来变为false,即中断状态会被清除。那么线程是否被中断过可以通过异常来判断。【同时要注意如果打断被join()wait() blocked 的线程也是一样会被清除,被清除 (interrupt status will be cleared) 的意思即中断状态设置为 false,被设置 (interrupt status will be set) 的意思就是中断状态设置为 true

1
2
3
4
5
6
17:06:11.890 [Thread-0] DEBUG com.concurrent.test.Test7 - 线程任务执行
17:06:12.387 [main] DEBUG com.concurrent.test.Test7 - 111是否被打断?false
17:06:12.390 [Thread-0] DEBUG com.concurrent.test.Test7 - 被打断
17:06:12.390 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?true
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 222是否被打断?false
17:06:12.890 [main] DEBUG com.concurrent.test.Test7 - 主线程

打断正常运行的线程

打断正常运行的线程, 线程并不会暂停,只是调用方法Thread.currentThread().isInterrupted();的返回值为true,可以判断Thread.currentThread().isInterrupted();的值来手动停止线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
}

终止模式之两阶段终止模式

Two Phase Termination,就是考虑在一个线程T1中如何优雅地终止另一个线程T2?这里的优雅指的是给T2一个料理后事的机会(如释放锁)。

如下所示:那么线程的 isInterrupted() 方法可以取得线程的打断标记,如果线程在睡眠 sleep 期间被打断,打断标记是不会变的,为 false,但是 sleep 期间被打断会抛出异常,我们据此手动设置打断标记为 true;如果是在程序正常运行期间被打断的,那么打断标记就被自动设置为true。处理好这两种情况那我们就可以放心地来料理后事啦!

image-20220814113307356

代码实现如下:

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
@Slf4j
public class Test11 {
public static void main(String[] args) throws InterruptedException {
TwoParseTermination twoParseTermination = new TwoParseTermination();
twoParseTermination.start();
Thread.sleep(3000); // 让监控线程执行一会儿
twoParseTermination.stop(); // 停止监控线程
}
}


@Slf4j
class TwoParseTermination{
Thread thread ;
public void start(){
thread = new Thread(()->{
while(true){
if (Thread.currentThread().isInterrupted()){
log.debug("线程结束。。正在料理后事中");
break;
}
try {
Thread.sleep(500);//情况1
log.debug("正在执行监控的功能");//情况2
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
});
thread.start();
}
public void stop(){
thread.interrupt();
}
}

打断 park 线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException {
test();
}

private static void test() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();

//sleep(1);
//t1.interrupt();

}

输出结果:

1
2
11:52:02.063 c.Test14 [t1] - park...
//线程无法向下运行

加入:t1.interrupt() 方法后

1
2
3
11:47:41.455 c.Test14 [t1] - park...
11:47:42.465 c.Test14 [t1] - unpark...
11:47:42.465 c.Test14 [t1] - 打断状态:true

如果打断标记已经是 true,则 park 会失效,这时使用 Thread.interrupted() 方法会清除打断标记,park 方法又生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static void test4() {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
log.debug("park...");
LockSupport.park();
log.debug("打断状态:{}", Thread.interrupted());
}
});
t1.start();


sleep(1);
t1.interrupt();
}

输出结果

1
2
3
4
11:54:04.225 c.Test14 [Thread-0] - park...
11:54:05.223 c.Test14 [Thread-0] - 打断状态:true
11:54:05.225 c.Test14 [Thread-0] - park...
//线程无法向下运行

不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

img

sleep,yiled,wait,join 对比

  • sleep 不释放锁、释放 cpu
  • join 释放锁、抢占 cpu
  • yiled 不释放锁、释放 cpu
  • wait 释放锁、释放 cpu

补充:

  1. sleep,join,yield,interrupted 是 Thread 类中的方法
  2. wait/notify 是 objec t中的方法

守护线程

默认情况下,java 进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完 java 进程也会停止。普通线程 t1 可以调用t1.setDeamon(true); 方法变成守护线程

注意

  • 垃圾回收器线程就是一种守护线程
  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

线程状态之五种状态

五种状态的划分主要是从操作系统的层面进行划分的

1583507073055

  1. 初始状态:仅仅是在语言层面上创建了线程对象,即Thead thread = new Thead();,还未与操作系统线程关联
  2. 可运行状态:也称就绪状态,指该线程已经被创建,与操作系统相关联,等待 cpu 给它分配时间片就可运行
  3. 运行状态:指线程获取了 CPU 时间片,正在运行
    • 当 CPU 时间片用完,线程会转换至【可运行状态】,等待 CPU 再次分配时间片,会导致我们前面讲到的上下文切换
  4. 阻塞状态
    • 如果调用了阻塞 API,如 BIO 读写文件,那么线程实际上不会用到 CPU,不会分配 CPU 时间片,会导致上下文切换,进入【阻塞状态】
    • 等待 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是,只要操作系统一直不唤醒线程,调度器就一直不会考虑调度它们,CPU 就一直不会分配时间片
  5. 终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

线程状态之六种状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址 根据 Thread.State 枚举,分为六种状态

1583507709834

  1. NEW 跟五种状态里的初始状态是一个意思
  2. RUNNABLE 是当调用了 start() 方法之后的状态,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
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
74
75
76
77
78
79
80
81
82
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};

Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable

}
}
};
t2.start();

Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();

Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();

Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();

Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();

try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());//NEW
log.debug("t2 state {}", t2.getState());//RUNNABLE
log.debug("t3 state {}", t3.getState());//TERMINATED
log.debug("t4 state {}", t4.getState());//TIMED_WAITING
log.debug("t5 state {}", t5.getState());//WAITING
log.debug("t6 state {}", t6.getState());//BLOCKED
System.in.read();
}
}

习题

阅读华罗庚《统筹方法》,给出烧水泡茶的多线程解决方案,提示

  • 参考图二,用两个线程(两个人协作)模拟烧水泡茶过程

    • 文中办法乙、丙都相当于任务串行

    • 而图一相当于启动了 4 个线程,有点浪费

  • 用 sleep(n) 模拟洗茶壶、洗水壶等耗费的时间

华罗庚《统筹方法》

统筹方法,是一种安排工作进程的数学方法。它的实用范围极广泛,在企业管理和基本建设中,以及关系复 杂的科研项目的组织与管理中,都可以应用。

怎样应用呢?主要是把工序安排好。

比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么办?

  • 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝。

  • 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡茶喝。

  • 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡茶喝。

哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。

这是小事,但这是引子,可以引出生产管理等方面有用的方法来。

水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:

image-20220814154609640

从这个图上可以一眼看出,办法甲总共要16分钟(而办法乙、丙需要20分钟)。如果要缩短工时、提高工作效率,应当主要抓烧开水这个环节,而不是抓拿茶叶等环节。同时,洗茶壶茶杯、拿茶叶总共不过4分钟,大可利用“等水开”的时间来做。

是的,这好像是废话,卑之无甚高论。有如走路要用两条腿走,吃饭要一口一口吃,这些道理谁都懂得。但稍有变化,临事而迷的情况,常常是存在的。在近代工业的错综复杂的工艺过程中,往往就不是像泡茶喝这么简单了。任务多了,几百几千,甚至有好几万个任务。关系多了,错综复杂,千头万绪,往往出现“万事俱备,只欠东风”的情况。由于一两个零件没完成,耽误了一台复杂机器的出厂时间。或往往因为抓的不是关键,连夜三班,急急忙忙,完成这一环节之后,还得等待旁的环节才能装配。

洗茶壶,洗茶杯,拿茶叶,或先或后,关系不大,而且同是一个人的活儿,因而可以合并成为:

image-20220814154547808

应用之统筹(烧水泡茶)

解法一:join

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

Thread t1 = new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(15);
}, "老王");

Thread t2 = new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("泡茶");
}, "小王");

t1.start();
t2.start();

输出

1
2
3
4
5
6
19:19:37.547 [小王] c.TestMakeTea - 洗茶壶
19:19:37.547 [老王] c.TestMakeTea - 洗水壶
19:19:38.552 [小王] c.TestMakeTea - 洗茶杯
19:19:38.552 [老王] c.TestMakeTea - 烧开水
19:19:40.553 [小王] c.TestMakeTea - 拿茶叶
19:19:53.553 [小王] c.TestMakeTea - 泡茶

解法1 的缺陷:

  • 上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶呢?代码最好能适应两种情况
  • 上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶呢?

解法二:wait/notify

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
class S2 {
static String kettle = "冷水";
static String tea = null;
static final Object lock = new Object();
static boolean maked = false;

public static void makeTea() {

new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(5);
synchronized (lock) {
kettle = "开水";
lock.notifyAll();
while (tea == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!maked) {
log.debug("拿({})泡({})", kettle, tea);
maked = true;
}
}
}, "老王").start();

new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
synchronized (lock) {
tea = "花茶";
lock.notifyAll();
while (kettle.equals("冷水")) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!maked) {
log.debug("拿({})泡({})", kettle, tea);
maked = true;
}
}
}, "小王").start();

}
}

输出:

1
2
3
4
5
6
20:04:48.179 c.S2 [小王] - 洗茶壶
20:04:48.179 c.S2 [老王] - 洗水壶
20:04:49.185 c.S2 [老王] - 烧开水
20:04:49.185 c.S2 [小王] - 洗茶杯
20:04:51.185 c.S2 [小王] - 拿茶叶
20:04:54.185 c.S2 [老王] - 拿(开水)泡(花茶)

解法2 解决了解法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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class S3 {

static String kettle = "冷水";
static String tea = null;
static final Object lock = new Object();

public static void makeTea() {

new Thread(() -> {
log.debug("洗水壶");
sleep(1);
log.debug("烧开水");
sleep(5);
synchronized (lock) {
kettle = "开水";
lock.notifyAll();
}
}, "老王").start();

new Thread(() -> {
log.debug("洗茶壶");
sleep(1);
log.debug("洗茶杯");
sleep(2);
log.debug("拿茶叶");
sleep(1);
synchronized (lock) {
tea = "花茶";
lock.notifyAll();
}
}, "小王").start();

new Thread(() -> {
synchronized (lock) {
while (kettle.equals("冷水") || tea == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("拿({})泡({})", kettle, tea);
}
}, "王夫人").start();

}
}

输出:

1
2
3
4
5
6
20:13:18.202 c.S3 [小王] - 洗茶壶
20:13:18.202 c.S3 [老王] - 洗水壶
20:13:19.206 c.S3 [小王] - 洗茶杯
20:13:19.206 c.S3 [老王] - 烧开水
20:13:21.206 c.S3 [小王] - 拿茶叶
20:13:24.207 c.S3 [王夫人] - 拿(开水)泡(花茶)

本章小结

本章的重点在于掌握

  • 线程创建

  • 线程重要 api,如 start,run,sleep,join,interrupt 等

  • 线程状态

  • 应用方面

    • 异步调用:主线程执行期间,其它线程异步执行耗时操作

    • 提高效率:并行计算,缩短运算时间

    • 同步等待:join

    • 统筹规划:合理使用线程,得到最优效果

  • 原理方面

    • 线程运行流程:栈、栈帧、上下文切换、程序计数器

    • Thread 两种创建方式 的源码

  • 模式方面

    • 终止模式之两阶段终止

文章作者: Yang Shiyu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yang Shiyu !
  目录