前言:文中出现的示例代码地址为:gitee代码地址

二、线程与进程

2.1 进程与线程

进程

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

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器(这里感觉要学了计算机组成原理之后会更有感觉吧!)

二者对比

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

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

进程间通信较为复杂

同一台计算机的进程通信称为 IPC(Inter-process communication)

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

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

2.2 并行与并发

并发

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

并行

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

二者对比

引用 Rob Pike 的一段描述:并发(concurrent)是同一时间应对(dealing with)多件事情的能力,并行(parallel)是同一时间动手做(doing)多件事情的能力

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
  • 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
  • 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)

2.3 应用

应用之异步调用(案例1)

以调用方角度来讲,如果

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

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

1) 设计

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

2) 结论

比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程

tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程

ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

应用之提高效率(案例1)

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

1
2
3
4
计算 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 才能提高效率,单核仍然时是轮流执行

2) 结论

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

  2. 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】),也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

  3. IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

三、java线程

本章内容

1、创建和运行线程

2、查看线程

3、线程 API

4、线程状态

3.1 创建和运行线程

方法一,直接使用 Thread

1
2
3
4
5
6
7
8
9
// 构造方法的参数是给线程指定名字,,推荐给线程起个名字
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
log.debug("hello");
}
};
t1.start();

方法二,使用 Runnable 配合 Thread(推荐)

把【线程】和【任务】(要执行的代码)分开,Thread 代表线程,Runnable 可运行的任务(线程要执行的代码)Test2.java

1
2
3
4
5
6
7
8
9
10
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐给线程起个名字
Thread t2 = new Thread(task2, "t2");
t2.start();

小结

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了,用 Runnable 更容易与线程池等高级 API 配合,用 Runnable 让任务类脱离了 Thread 继承体系,更灵活。通过查看源码可以发现,方法二其实到底还是通过方法一执行的!

使用Lambda接口

1
2
3
4
5
6
Runnable task2 = () -> {Log.debug("running");};
Thread t2 = new Thread(task2, "t2");
t2.start();

// 或者
Thread t2 = new Thread(() -> {Log.debug("running");}, "t2");

原理之 Thread 与 Runnable 的关系

方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了

用 Runnable 更容易与线程池等高级 API 配合

用 Runnable 让任务类脱离了 Thread 继承体系,更灵活

方法三,FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况 Test3.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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("主线程");
// get()方法用来(阻塞)等待 返回的结果
// 即 阻塞等待
log.debug("{}",futureTask.get());
}

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过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;
}

Future提供了三种功能: FutureTask是Future和Runable的实现 

  1. 判断任务是否完成;   

  2. 能够中断任务;   

  3. 能够获取任务执行结果。

3.2 线程运行原理

虚拟机栈与栈帧

虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作数栈、动态链接、方法返回值、一些附加信息,是属于线程的私有的。当java中使用多线程时,每个线程都会维护它自己的栈帧!

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

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

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

  • 线程的 cpu 时间片用完(每个线程轮流执行,看前面并行的概念)
  • 垃圾回收
  • 有更高优先级的线程需要运行
  • 线程自己调用了 sleepyieldwaitjoinparksynchronizedlock 等方法

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

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能

3.3 ✨✨Thread的常见方法

①常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象并命名
Thread(Runnable target,String name)使用 Runnable 对象创建线程对象并命名

线程创建根本上来讲有两种创建方法:

  1. 创建一个继承自 Thread 类的子类,重写 Thread 中的 run 方法,调用 start 方法
  2. 创建一个实现 Runnable 接口的类,重写 Thread 中的 run 方法。创建 Thread 实例,将自己写的实现 Runnable 接口的类的实例设置进去,调用 start 方法

②常见属性

属性方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否为后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

解释:

  1. 线程的唯一标识就是线程 Id

  2. 名称在上面的案例中有所体现,为调试提供了便利

  3. 状态表示线程当前所处的情况,上面的案例中,线程因为调用了 sleep 方法就进入了阻塞状态

  4. 优先级表示该线程被调度到的难易度,高的更容易被调度到

  5. 判断是否为后台线程。如果是后台线程,那么该后台线程不会影响 java 进程的结束;如果是非后台线程,JVM 就会等到所有的非后台线程执行完毕,才会结束运行,因此会影响到总进程的结束

  6. 是否存活是来判断线程是否还存在的方法。当创建出 Thread 实例对象时,线程未必就创建了,需要调用 start 方法,才是真正的创建了线程。当线程中的 run 方法执行完毕,线程结束被销毁了,创建出的实例对象还没被销毁回收。所以说,创建出的实例对象和线程的生命周期是不完全相同的

    在线程的状态中,除了NEWTERMINATED以外的状态都是活着的

③创建线程

创建 Thread 类的对象不意味着线程被创建出,start() 方法才是真正的在操作系统内部创建一个新的线程,通过重写 run 方法来描述需要执行的任务,从而真正实现了多线程运行。

④中断线程

方法一:手动设置标志位,作为中断线程的条件

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
public class func9 {
private static Boolean flag = false;//手动设置的标志位 flag
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (!flag) {
//flag 为真时停止循环
System.out.println("myThread");
try {
Thread.sleep(1000);//打印一次,阻塞一秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();//创建了线程 t
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
//等3秒后,在主线程中将 flag 的值改成 true,从而使线程t循环条件不成立
}
}

方法二:使用 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
27
28
public class func10 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
//通过 isInerrupted()判断标志位是否为true,为true说明线程要退出
while (!this.isInterrupted()) {
System.out.println("my Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//System.out.println("完善工作");
//break;
}
}
}
};
t.start();//创建新的线程
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.interrupt();
//t线程运行3秒后,通过 interrupt() 方法将标志位设置为 true
}
}

⑤线程等待

通过join() 方法来决定线程执行顺序(主要控制结束线程的顺序)。

  • 在没有调用 join 方法时,main 线程和 t1 线程是并发的,结果输出是相间的。调用 join 方法后,main 线程就会阻塞等待,要等 t1 线程执行完毕,才会执行 join 方法后的内容

⑥获取线程引用

方法一:通过继承 Thread 类创建的线程,可以在重写的 run 方法中通过 this获取当前线程的实例

方法二:通过 Thread 类的 currentThread() 方法,哪个线程调用了该方法,就返回哪个线程的实例对象

⑦线程休眠

该方法在前面经常介绍,那就是 sleep 方法

一旦调用 sleep 方法,线程就会阻塞等待,等待的时间取决于指定的参数

操作系统是以线程为单位进行调度的,每个线程都对应着一个 PCB,并通过双向链表组织这些 PCB

操作系统调度 PCB 时,就是从就绪队列中选出一个 PCB 去 CPU 上执行,当执行着的线程调用了 sleep 方法,这个 PCB 就会被移动到阻塞队列中,等到 sleep 的时间到了,就会回到就绪队列中,准备好被执行

join 方法也会产生阻塞等待,就像线程等待中的例子,main 线程执行到 join方法后,就到阻塞队列中,等待对应的 t1 线程执行完毕,才会回到就绪队列中,做好被执行的准备

⑧线程状态

1
2
3
4
5
6
7
8
9
//线程的状态是一个枚举类型 Thread.State
//打印 Java 线程中的所有状态
public class func13 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
  • NEW:表示 Thread 类的对象创建出,但是线程还没有被创建,即没有调用 start 方法
  • RUNNABLE:就绪状态
  • BLOCKED:等待锁时的状态
  • WAITING:通过 wait 方法触发
  • TIMED_WAITING:通过 sleep 方法产生
  • TERMINATED:线程已经执行完毕,但 Thread 类的对象还存在,未被销毁

3.3.1 start 与 run

调用start

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

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

1
2
3
4
11:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
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.Test4 - 我是一个新建的线程正在运行中
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.Test4 - 主线程

小结

直接调用 run() 是在主线程中执行了 run(),没有启动新的线程 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法 中的代码

3.3.2 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时间片

3.3.3 线程优先级setPriority

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
// t1.setPriority(Thread.MIN_PRIORITY);
// t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

3.3.4 join

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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("结束");
}

3.3.5 interrupt 方法详解

打断 sleep,wait,join 的线程

先了解一些interrupt()方法的相关知识:博客地址

sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态,以 sleep 为例Test7.java

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。处理好这两种情况那我们就可以放心地来料理后事啦!

代码实现如下:

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
@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);
log.debug("正在执行监控的功能");
} catch (InterruptedException e) {
//重新设置打断标记,使自己在下一次循环时料理后事
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
});
thread.start();
}
public void stop(){
thread.interrupt();
}
}

打断 park 线程

打断 park 线程, 不会清空打断状态

  • 如果打断标记已经是 true, 则 park 会失效
1
2
3
4
5
6
7
8
9
10
11
private static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(0.5);
t1.interrupt();
}

输出结果

1
2
3
21:11:52.795 [t1] c.TestInterrupt - park... 
21:11:53.295 [t1] c.TestInterrupt - unpark...
21:11:53.295 [t1] c.TestInterrupt - 打断状态:true

3.3.6 sleep,yield,wait,join 对比

关于join的原理和这几个方法的对比:看这里

补充:

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

sleep 不释放锁、释放cpu

join 释放锁、抢占cpu

yiled 不释放锁、释放cpu

wait 释放锁、释放cpu

3.4 守护线程

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

注意

垃圾回收器线程就是一种守护线程

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

3.5 线程状态之五种状态

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

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

3.6 线程状态之六种状态

这是从 Java API 层面来描述的,我们主要研究的就是这种。状态转换详情图:地址

根据 Thread.State 枚举,分为六种状态 Test12.java

  1. NEW 线程刚被创建,但是还没有调用 start() 方法
  2. RUNNABLE 是当调用了 start() 方法之后的状态
    • 注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统层面的【可运行状态】、【运行状态】和【io阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
  3. BLOCKEDWAITINGTIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
  4. TERMINATED 当线程代码运行结束

  • NEW:表示 Thread 类的对象创建出,但是线程还没有被创建,即没有调用 start 方法

  • RUNNABLE:就绪状态

  • BLOCKED:等待锁时的状态

  • WAITING:通过 wait 方法触发

  • TIMED_WAITING:通过 sleep 方法产生

  • TERMINATED:线程已经执行完毕,但 Thread 类的对象还存在,未被销毁

本章小结

本章的重点在于掌握

  • 线程创建

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

  • 线程状态

应用方面

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

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

  • 同步等待:join

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

原理方面

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

  • Thread 两种创建方式 的源码

模式方面

  • 终止模式之两阶段终止