1. 进程与线程
1.1 进程与线程
【1】进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
【2】线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在 windows 中进程是不活动的,只是作为线程的容器
【3】二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为 IPC(Inter-process communication)
- 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.2 并行与并发
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。总结为一句话就是: 微观串行,宏观并行 ,
一般会将这种 线程轮流使用 CPU 的做法称为并发,concurrent
CPU | 时间片1 | 时间片2 | 时间片3 | 时间片4 |
---|---|---|---|---|
core | 线程1 | 线程2 | 线程3 | 线程4 |
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的。
CPU | 时间片1 | 时间片2 | 时间片3 | 时间片4 |
---|---|---|---|---|
core1 | 线程1 | 线程2 | 线程3 | 线程4 |
core2 | 线程1 | 线程2 | 线程3 | 线程4 |
引用 Rob Pike 的一段描述:
- 并发(concurrent) 是同一时间应对(dealing with)多件事情的能力
- 并行(parallel) 是同一时间动手做(doing)多件事情的能力
例子
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
- 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
1.3 应用
【1】应用之异步调用
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
1) 设计
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
2) 结论
比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
【2】应用之提高效率
充分利用多核 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.单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活
- 2.多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的
- 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)
- 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
- 3.IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
2. Java线程
2.1 创建和运行线程
【1】直接使用Thread
@Slf4j
public class ThreadTest {
public static void main(String[] args) {
// 创建线程对象,构造方法参数(t1)是给线程指定名字
Thread t = new Thread("t1"){
@Override
public void run() {
// run方法内实现了要执行的任务
log.info("线程执行=========");
}
};
t.start();
log.info("main方法执行============");
}
}
输出:
16:57:28.678 [main] INFO com.wyc.test.ThreadTest - main方法执行============
16:57:28.678 [t1] INFO com.wyc.test.ThreadTest - 线程执行=========
【2】使用 Runnable 配合 Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
@Slf4j
public class ThreadAndRunnable {
public static void main(String[] args) {
// 创建任务对象
Runnable r = new Runnable() {
@Override
public void run() {
// 要执行的任务
log.info("Runnable执行=======");
}
};
// 创建线程对象, 参数1是任务对象, 参数2是线程名字
Thread t = new Thread(r,"t2");
// 启动线程
t.start();
}
}
Java 8 以后可以使用 lambda 精简代码
// Java 8 以后可以使用 lambda 精简代码
Runnable r = () -> {
// 要执行的任务
log.info("Runnable执行=======");
};
Thread 与 Runnable 的关系
- Thread 是把线程和任务合并在了一起,Runnable 是把线程和任务分开了
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
【3】FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
@Slf4j
public class ThreadAndFutureTask {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.info("FutureTask执行=======");
Thread.sleep(1000);
return 100;
}
});
Thread t = new Thread(task,"t1");
t.start();
Integer result = task.get();
log.info("获取到返回结果:{}", result);
}
}
输出结果:
17:51:53.245 [t1] INFO com.wyc.test.ThreadAndFutureTask - FutureTask执行=======
17:51:54.249 [main] INFO com.wyc.test.ThreadAndFutureTask - 获取到返回结果:100
2.2 查看进程线程的方法
【1】windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
【2】 linux
- ps -ef 查看所有进程
- ps -fT -p 查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
- top -H -p 查看某个进程(PID)的所有线程
【3】Java
- jps 命令查看所有 Java 进程
- jstack 查看某个 Java 进程(PID)的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
2.3 原理之线程运行
【1】栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
public class TestFrames {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
【2】线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
2.4 常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() | 启动一个新线程,在新的线程运行 run 方法中的代码 | start 方法只是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的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() | 判断是否被打断 | 不会清除 打断标记 | |
isAlive() | 线程是否存活(还没有运行完毕) | ||
interrupt() | 打断线程 | 如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException,并清除 打断标记 ;如果打断的正在运行的线程,则会设置 打断标记 ;park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
currentThread() | static | c 获取当前正在执行的线程 | |
sleep(long n) | static | 让当前执行的线程休眠n毫秒,休眠时让出 cpu 的时间片给其它线程 | |
yield() | static | 提示线程调度器让出当前线程对CPU的使用 | 主要是为了测试和调试 |
2.5 start 与 run
调用 run
@Slf4j
public class StartAndRun {
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
FileReader.read();
log.info("线程执行===");
}
};
t1.run();
log.info("其他事情===");
}
}
输出
22:08:51.815 [main] DEBUG com.wyc.n2.util.FileReader - 做了其他事共花费: 2001 ms
22:08:51.829 [main] INFO com.wyc.test.StartAndRun - 线程执行===
22:08:51.829 [main] INFO com.wyc.test.StartAndRun - 其他事情===
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用 start
将上述代码的 t1.run() 改为 t1.start();
输出
22:10:22.051 [main] INFO com.wyc.test.StartAndRun - 其他事情===
22:10:24.051 [t1] DEBUG com.wyc.n2.util.FileReader - 做了其他事共花费: 2001 ms
22:10:24.078 [t1] INFO com.wyc.test.StartAndRun - 线程执行===
程序在 t1 线程运行, FileReader.read() 方法调用是异步的
小节
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
2.6 sleep 与 yield
【1】sleep
- 1.调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 2.其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 3.睡眠结束后的线程未必会立刻得到执行
- 4.建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
@Slf4j
public class SleepTest {
public static void main(String[] args) throws InterruptedException {
log.info("睡眠前===");
Thread.sleep(1000);
// 另一种睡眠方式写法
TimeUnit.SECONDS.sleep(1);
log.info("睡眠后===");
}
}
【2】yield
- 1.调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 2.具体的实现依赖于操作系统的任务调度器
【3】线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
// 设置线程优先级
thread.setPriority(Thread.MAX_PRIORITY);
【4】限制对 CPU 的使用
sleep 实现
在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权给其他程序
while(true) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 可以用 wait 或 条件变量达到类似的效果
- 不同的是,后两种都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景
- sleep 适用于无需锁同步的场景
2.7 join 方法详解
下面的代码执行,打印 r 是什么?
@Slf4j
public class JoinTest {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.info("方法开始===");
Thread t1 = new Thread("t1"){
@SneakyThrows
@Override
public void run() {
log.info("线程开始==");
sleep(1);
log.info("线程结束==");
r = 10;
}
};
t1.start();
log.info("结果为:{}",r);
log.info("方法结束===");
}
}
分析
- 因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
- 而主线程一开始就要打印 r 的结果,所以只能打印出 r=0
解决方法
- 用 sleep 行不行?为什么?
- 用 join,加在 t1.start() 之后即可
t1.join();
2.8 interrupt 方法详解
【1】打断 sleep,wait,join 的线程
这几个方法都会让线程进入阻塞状态
打断 sleep 的线程, 会清空打断状态,以 sleep 为例
@Slf4j
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
log.info("睡眠==");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
log.info("打断==");
t1.interrupt();
log.info("打断标记:{}",t1.isInterrupted());
}
}
输出
17:58:08.409 [t1] INFO com.wyc.test.InterruptTest - 睡眠==
17:58:09.407 [main] INFO com.wyc.test.InterruptTest - 打断==
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.wyc.test.InterruptTest$1.run(InterruptTest.java:17)
17:58:09.407 [main] INFO com.wyc.test.InterruptTest - 打断标记:true
【2】打断正常运行的线程
@Slf4j
public class InterruptTest1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
while(true){
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
log.info("被打断了,退出循环");
break;
}
}
}
};
t1.start();
Thread.sleep(1000);
log.info("打断==");
t1.interrupt();
log.info("打断标记:{}",t1.isInterrupted());
}
}
输出:
20:13:30.680 [main] INFO com.wyc.test.InterruptTest1 - 打断==
20:13:30.682 [t1] INFO com.wyc.test.InterruptTest1 - 被打断了,退出循环
20:13:30.682 [main] INFO com.wyc.test.InterruptTest1 - 打断标记:true
2.9 主线程与守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
@Slf4j
public class Test00 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
if (Thread.currentThread().isInterrupted()){
log.info("线程退出==");
break;
}
}
},"t1");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.info("main方法结束");
}
}
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
2.10 五种状态
这是从操作系统层面来描述的
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.11 六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
- TERMINATED 当线程代码运行结束
代码例子:
@Slf4j
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());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
}
}
输出:
21:50:52.644 [t3] DEBUG com.wyc.n3.TestState - running...
21:50:53.144 [main] DEBUG com.wyc.n3.TestState - t1 state NEW
21:50:53.156 [main] DEBUG com.wyc.n3.TestState - t2 state RUNNABLE
21:50:53.156 [main] DEBUG com.wyc.n3.TestState - t3 state TERMINATED
21:50:53.156 [main] DEBUG com.wyc.n3.TestState - t4 state TIMED_WAITING
21:50:53.156 [main] DEBUG com.wyc.n3.TestState - t5 state WAITING
21:50:53.156 [main] DEBUG com.wyc.n3.TestState - t6 state BLOCKED
【1】情况1 NEW --> RUNNABLE
- 当调用 t.start() 方法时,由 NEW --> RUNNABLE
【2】情况2 RUNNABLE <–> WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
- 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 WAITING --> BLOCKED
【3】情况3 RUNNABLE <–> WAITING
- 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
- 注意是当前线程在t 线程对象的监视器上等待
- t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE
【4】情况4 RUNNABLE <–> WAITING
- 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE
【5】情况5 RUNNABLE <–> TIMED_WAITING
t 线程用 synchronized(obj) 获取了对象锁后
- 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
- t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时
- 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED
【6】情况6 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
- 注意是当前线程在t 线程对象的监视器上等待
- 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE
【7】情况7 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE
【8】情况8 RUNNABLE <–> TIMED_WAITING
- 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
- 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING–> RUNNABLE
【9】情况9 RUNNABLE <–> BLOCKED
- t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
- 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED
【10】情况10 RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
3. 共享模型之管程
3.1 synchronized
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized语法
synchronized(对象){
临界区
}
@Slf4j
public class Test01 {
static int count = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count ++;
}
}
}, "t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lock){
count --;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("{}",count);
}
}
你可以做这样的类比:
- synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人
- 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码
- 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了
- 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),
- 这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码
面向对象改进
把需要保护的共享变量放入一个类
@Slf4j
public class Test01 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("{}",room.getCount());
}
}
class Room {
int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public void decrement() {
synchronized (this) {
count--;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
3.2 方法上的 synchronized
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
【1】线程八锁
情况1:先1后2 或 先2后1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n1.b();}).start();
}
}
@Slf4j
class Number{
public synchronized void a(){
log.info("1");
}
public synchronized void b(){
log.info("2");
}
}
情况2:1s后12,或 2 1s后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n1.b();}).start();
}
}
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b(){
log.info("2");
}
}
情况3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n1.b();}).start();
new Thread(()->{ n1.c();}).start();
}
}
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b(){
log.info("2");
}
public void c(){
log.info("3");
}
}
情况4:2 1s 后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n2.b();}).start();
}
}
@Slf4j
class Number{
public synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b(){
log.info("2");
}
}
情况5:2 1s 后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n1.b();}).start();
}
}
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b(){
log.info("2");
}
}
情况6:1s 后12, 或 2 1s后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n1.b();}).start();
}
}
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public static synchronized void b(){
log.info("2");
}
}
情况7:2 1s 后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n2.b();}).start();
}
}
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public synchronized void b(){
log.info("2");
}
}
情况8:1s 后12, 或 2 1s后 1
@Slf4j
public class Test8Locks {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a();}).start();
new Thread(()->{ n2.b();}).start();
}
}
@Slf4j
class Number{
public static synchronized void a(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("1");
}
public static synchronized void b(){
log.info("2");
}
}
3.3 变量的线程安全分析
【1】成员变量和静态变量是否线程安全?
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
【2】局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
【3】局部变量线程安全分析
【3.1】成员变量
@Slf4j
public class TestThreadSafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public final void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
执行之后有两种情况,一种是正常执行,另一种是,如果线程2 还未 add,线程1 remove 就会报错:
Exception in thread "Thread1" java.lang.IndexOutOfBoundsException: Index: 0, Size: 0
at java.util.ArrayList.rangeCheck(ArrayList.java:653)
at java.util.ArrayList.remove(ArrayList.java:492)
at com.wyc.test.ThreadSafe.method3(TestThreadSafe.java:39)
at com.wyc.test.ThreadSafe.method1(TestThreadSafe.java:30)
at com.wyc.test.TestThreadSafe.lambda$main$0(TestThreadSafe.java:19)
at java.lang.Thread.run(Thread.java:745)
分析:
- 无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量
- method3 与 method2 分析相同
【3.2】将 list 修改为局部变量
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
分析:
- list 是局部变量,每个线程调用时会创建其不同实例,没有共享
- 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
- method3 的参数分析与 method2 相同
【4】常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
3.4 wait notify
【1】API介绍
- obj.wait() 让进入 object 监视器的线程到 waitSet 等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
@Slf4j
public class Test02 {
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (lock){
log.debug("执行.......");
try {
// 让线程在obj上一直等待下去
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码......");
}
}, "t1").start();
new Thread(()->{
synchronized (lock){
log.debug("执行.......");
try {
// 让线程在obj上一直等待下去
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其他代码......");
}
},"t2").start();
//主线程两秒后执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("唤醒obj上其他线程");
synchronized (lock){
// lock.notify(); // 唤醒obj上一个线程
lock.notifyAll(); //唤醒obj上所有等待线程
}
}
}
【2】sleep() 和 wait()的区别
- sleep 是 Thread 方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁 4) 它们状态 TIMED_WAITING
- 它们状态都是TIMED_WAITING
3.5 ReentrantLock
相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
【1】可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
@Slf4j
public class Test05 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("进入main");
m1();
} catch (Exception e) {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
log.debug("进入m1");
m2();
} catch (Exception e) {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
log.debug("进入m2");
} catch (Exception e) {
lock.unlock();
}
}
}
输出:
21:21:46.441 [main] DEBUG com.wyc.test.Test05 - 进入main
21:21:46.445 [main] DEBUG com.wyc.test.Test05 - 进入m1
21:21:46.445 [main] DEBUG com.wyc.test.Test05 - 进入m2
【2】可打断
@Slf4j
public class Test06 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争那么此方法就会获取lock对象锁
// 如果有竞争就进入阻塞队列,可以被其他线程用interrupt方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获取到锁,返回");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("打断t1");
t1.interrupt();
}
}
输出
21:28:51.461 [t1] DEBUG com.wyc.test.Test06 - 尝试获取锁
21:28:52.457 [main] DEBUG com.wyc.test.Test06 - 打断t1
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.wyc.test.Test06.lambda$main$0(Test06.java:21)
at java.lang.Thread.run(Thread.java:745)
21:28:52.458 [t1] DEBUG com.wyc.test.Test06 - 没有获取到锁,返回
【3】锁超时
立刻失败
@Slf4j
public class Test07 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
if (!lock.tryLock()){
log.debug("获取不到锁");
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
}
}
输出
21:33:16.078 [t1] DEBUG com.wyc.test.Test07 - 尝试获取锁
21:33:16.081 [t1] DEBUG com.wyc.test.Test07 - 获取不到锁
超时失败
@Slf4j
public class Test07 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)){
log.debug("获取等待 1s 后失败,返回");
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
}
}
输出
21:35:20.894 [t1] DEBUG com.wyc.test.Test07 - 尝试获取锁
21:35:21.899 [t1] DEBUG com.wyc.test.Test07 - 获取等待 1s 后失败,返回
21:35:21.899 [t1] DEBUG com.wyc.test.Test07 - 获取不到锁
【4】公平锁
ReentrantLock 默认是不公平的
改为公平锁
ReentrantLock lock = new ReentrantLock(true);
公平锁一般没有必要,会降低并发度
【5】条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
@Slf4j
public class Test08 {
static boolean hasCigarette = false;
static boolean hasTakeout = false;
private static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
// 等待外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(()->{
ROOM.lock();
try {
log.debug("有烟没?[{}]",hasCigarette);
while (!hasCigarette){
log.debug("没烟,先歇会!");
try {
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了....");
} finally {
ROOM.unlock();
}
},"小南").start();
new Thread(()->{
ROOM.lock();
try {
log.debug("外卖送到没?[{}]",hasTakeout);
while (!hasTakeout){
log.debug("没外卖,先歇会!");
try {
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了....");
} finally {
ROOM.unlock();
}
},"小女").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
ROOM.lock();
try {
hasTakeout = true;
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
},"送外卖的").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
ROOM.lock();
try {
hasCigarette = true;
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
},"送烟的").start();
}
}
输出
22:11:01.399 [小南] DEBUG com.wyc.test.Test08 - 有烟没?[false]
22:11:01.412 [小南] DEBUG com.wyc.test.Test08 - 没烟,先歇会!
22:11:01.412 [小女] DEBUG com.wyc.test.Test08 - 外卖送到没?[false]
22:11:01.412 [小女] DEBUG com.wyc.test.Test08 - 没外卖,先歇会!
22:11:02.399 [小女] DEBUG com.wyc.test.Test08 - 可以开始干活了....
22:11:03.399 [小南] DEBUG com.wyc.test.Test08 - 可以开始干活了....
3.6 交替输出
【1】 synchronized
@Slf4j
public class Test09 {
public static void main(String[] args) {
WaitNotify wn = new WaitNotify(1,5);
new Thread(()->{
wn.print("a", 1, 2);
}).start();
new Thread(()->{
wn.print("b", 2, 3);
}).start();
new Thread(()->{
wn.print("c", 3, 1);
}).start();
}
}
class WaitNotify{
// 打印
public void print(String str, int waitFlag, int nextFlag){
for (int i = 0; i < loopNumber; i++) {
synchronized (this){
while (flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(str);
flag = nextFlag;
this.notifyAll();
}
}
}
// 等待标记
private int flag;
// 循环次数
private int loopNumber;
public WaitNotify(int flag, int loopNumber){
this.flag = flag;
this.loopNumber = loopNumber;
}
}
【2】ReentrantLock
@Slf4j
public class Test10 {
public static void main(String[] args) {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a",a, b);
}).start();
new Thread(()->{
awaitSignal.print("b",b, c);
}).start();
new Thread(()->{
awaitSignal.print("c",c, a);
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
log.debug("开始.....");
a.signal();
} finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock{
private int loopNumber;
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
// 参数1:打印内容 参数2: 进入哪一间休息 参数3:下一件休息室
public void print(String str, Condition current, Condition next){
for (int i = 0; i < loopNumber; i++) {
lock();
try {
current.await();
System.out.println(str);
next.signal();
}catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
【3】Park
@Slf4j
public class Test11 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) {
ParkUnpark pu = new ParkUnpark(5);
t1 = new Thread(()->{
pu.print("a",t2);
});
t2 = new Thread(()->{
pu.print("b",t3);
});
t3 = new Thread(()->{
pu.print("c",t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
}
}
class ParkUnpark{
public void print(String str, Thread next){
for (int i = 0; i < loopNumber; i++) {
LockSupport.park();
System.out.println(str);
LockSupport.unpark(next);
}
}
private int loopNumber;
public ParkUnpark(int loopNumber){
this.loopNumber = loopNumber;
}
}
4. 共享模型之内存
4.1 Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。
JMM 体现在以下几个方面
- 原子性 - 保证指令不会受到线程上下文切换的影响
- 可见性 - 保证指令不会受 cpu 缓存的影响
- 有序性 - 保证指令不会受 cpu 指令并行优化的影响
4.2 可见性
【1】退不出的循环
@Slf4j
public class Test12 {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (run){
}
});
t.start();
Thread.sleep(1000);
log.debug("停止t");
run = false; // 线程t不会如预想的停下来
}
}
为什么呢?分析一下:
1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
【2】解决方法
volatile(易变关键字)
volatile static boolean run = true;
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
4.3 有序性
JVM 会在不影响正确性的前提下,可以调整语句的执行顺序
这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。
5. 线程池
5.1 自定义线程池
@Slf4j
public class TestPool {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10,(queue, task)->{
// 1. 死等
// queue.put(task);
// 2. 带超时等待
// queue.offer(task, 500, TimeUnit.MILLISECONDS);
// 3. 让调用者放弃任务执行
// log.debug("放弃{}",task);
// 4. 让调用者抛出异常
// throw new RuntimeException("任务执行失败" + task);
// 5. 让调用者自己执行任务
task.run();
});
for (int i = 0; i < 5; i++) {
int j = i;
threadPool.execute(() -> {
log.debug("{}",j);
});
}
}
}
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T>{
void reject(BlockingQueue<T> queue, T task);
}
@Slf4j
class ThreadPool{
// 任务队列
private BlockingQueue<Runnable> taskQueue;
// 线程集合
private HashSet<Worker> workers = new HashSet<>();
// 核心线程数
private int coreSize;
// 获取任务的超时时间
private long timeout;
private TimeUnit timeUnit;
private RejectPolicy<Runnable> rejectPolicy;
// 执行任务
public void execute(Runnable task){
// 当任务数没有超过coreSize时,直接交给worker对象执行
// 如果超过coreSize时,加入任务队列暂存
synchronized (workers) {
if (workers.size() < coreSize){
Worker worker = new Worker(task);
log.debug("新增 worker 对象{},{}",worker,task);
workers.add(worker);
worker.start();
}else {
log.debug("加入任务队列{}",task);
taskQueue.put(task);
// 1. 死等
// 2. 带超时等待
// 3. 让调用者放弃任务执行
// 4. 让调用者抛出异常
// 5. 让调用者自己执行任务
taskQueue.tryPut(rejectPolicy, task);
}
}
}
public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapcity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task){
this.task = task;
}
@Override
public void run() {
// 执行任务
// 1) 当task不为空,执行任务
// 2) 当task执行完毕,再接着从任务队列获取任务并执行
// while (task != null || (task = taskQueue.take()) != null){
while (task != null || (task = taskQueue.poll(timeout, timeUnit)) != null){
try{
log.debug("正在执行....{}",task);
task.run();
}catch (Exception e){
e.printStackTrace();
}finally {
task = null;
}
}
synchronized (workers){
log.debug("woker被移除{}",this);
workers.remove(this);
}
}
}
}
@Slf4j
class BlockingQueue<T>{
// 1.任务队列
private Deque<T> queue = new ArrayDeque<>();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3. 生产者条件变量
private Condition fullWaitSet = lock.newCondition();
// 4. 消费者条件变量
private Condition emptyWaitSet = lock.newCondition();
// 5. 容量
private int capcity;
public BlockingQueue(int capcity) {
this.capcity = capcity;
}
// 带超时的阻塞获取
public T poll(long timeout, TimeUnit unit){
lock.lock();
try {
// 将timeout 统一转换为纳秒
long nanos = unit.toNanos(timeout);
while (queue.isEmpty()){
try {
// 返回的是剩余时间
if (nanos <= 0){
return null;
}
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞获取
public T take(){
lock.lock();
try {
while (queue.isEmpty()){
try {
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// 阻塞添加
public void put(T element){
lock.lock();
try {
while (queue.size() == capcity){
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.addLast(element);
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 带超时时间的阻塞添加
public boolean offer(T task, long timeout, TimeUnit timeUnit){
lock.lock();
try {
long nanos = timeUnit.toNanos(timeout);
while (queue.size() == capcity){
try {
log.debug("等待加入任务队列{}...",task);
if (nanos <= 0){
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("加入队列{}", task);
queue.addLast(task);
emptyWaitSet.signal();
return true;
} finally {
lock.unlock();
}
}
// 获取大小
public int size(){
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
public void tryPut(RejectPolicy<T> rejectPolicy, T task) {
lock.lock();
try {
// 判断队列是否已满
if (queue.size() == capcity){
rejectPolicy.reject(this, task);
}else { // 有空闲
log.debug("加入队列{}", task);
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
}
5.2 ThreadPoolExecutor
【1】线程池状态
ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量
状态名 | 高3位 | 接收新任务 | 处理阻塞队列任务 | 说明 |
---|---|---|---|---|
RUNNING | 111 | Y | Y | |
SHUTDOWN | 000 | N | Y | 不会接收新任务,但会处理阻塞队列剩余任务 |
STOP | 001 | N | N | 会中断正在执行的任务,并抛弃阻塞队列任务 |
TIDYING | 010 | - | - | 任务全执行完毕,活动线程为 0 即将进入终结 |
TERMINATED | 011 | - | - | 终结状态 |
从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
【2】 构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目 (最多保留的线程数)
- maximumPoolSize 最大线程数目
- keepAliveTime 生存时间 - 针对救急线程
- unit 时间单位 - 针对救急线程
- workQueue 阻塞队列
- threadFactory 线程工厂 - 可以为线程创建时起个好名字
- handler 拒绝策略
工作方式
核心线程是一直存在的, 救急线程是有存活时间的
- 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。
- 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。
- 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。
- 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现
- AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy 让调用者运行任务
- DiscardPolicy 放弃本次任务
- DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
- Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
- Netty 的实现,是创建一个新线程来执行任务
- ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
- PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
- 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。
根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池
【3】 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
特点
- 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
- 阻塞队列是无界的,可以放任意数量的任务
评价: 适用于任务量已知,相对耗时的任务
@Slf4j
public class TestThreadPoolExecutors {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "mypool_t" + t.getAndIncrement());
}
});
pool.execute(()->{
log.debug("1");
});
pool.execute(()->{
log.debug("2");
});
pool.execute(()->{
log.debug("3");
});
}
}
【4】newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
特点
- 核心线程数是 0, 最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收),救急线程可以无限创建
- 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)
评价
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况
【5】newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
使用场景:
希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
- Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改
- FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
- Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改
- 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
【6】提交任务
// 执行任务
void execute(Runnable command);
// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);
// 提交 tasks 中所有任务
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
【6.1】execute
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.execute(new Runnable() {
@Override
public void run() {
log.debug("begin.....");
}
});
}
【6.2】submit
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<String> future = pool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("running.....");
Thread.sleep(1000);
return "ok";
}
});
log.debug("{}", future.get());
}
【6.3】invokeAll
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
List<Future<String>> futures = pool.invokeAll(Arrays.asList(
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(1000);
return "1";
}
},
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(500);
return "2";
}
},
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(2000);
return "3";
}
}
));
for (Future<String> future : futures) {
log.debug("{}", future.get());
}
}
【6.4】invokeAny
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(2);
String result = pool.invokeAny(Arrays.asList(
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(1000);
log.debug("end");
return "1";
}
},
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(500);
log.debug("end");
return "2";
}
},
new Callable<String>() {
@Override
public String call() throws Exception {
log.debug("begin");
Thread.sleep(2000);
log.debug("end");
return "3";
}
}
));
log.debug("{}",result);
}
【7】关闭线程池
【7.1】shutdown
/*
线程池状态变为 SHUTDOWN
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();
【7.2】shutdownNow
/*
线程池状态变为 STOP
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
@Slf4j
public class TestShutDown {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> result1 = pool.submit(() -> {
log.debug("task 1 running .....");
Thread.sleep(1000);
log.debug("task 1 finish ......");
return 1;
});
Future<Integer> result2 = pool.submit(() -> {
log.debug("task 2 running .....");
Thread.sleep(1000);
log.debug("task 2 finish ......");
return 2;
});
Future<Integer> result3 = pool.submit(() -> {
log.debug("task 3 running .....");
Thread.sleep(1000);
log.debug("task 3 finish ......");
return 3;
});
log.debug("shutdown");
// pool.shutdown();
// pool.awaitTermination(3, TimeUnit.SECONDS);
List<Runnable> runnables = pool.shutdownNow();
log.debug("runnables{}",runnables);
log.debug("other....");
}
}
【8】任务调度线程池
在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。
【8.1】Timer
@Slf4j
public class TestTimer {
public static void main(String[] args) {
Timer timer = new Timer();
TimerTask task1 = new TimerTask() {
@SneakyThrows
@Override
public void run() {
log.debug("task 1");
Thread.sleep(2000);
}
};
TimerTask task2 = new TimerTask() {
@Override
public void run() {
log.debug("task 2");
}
};
log.debug("start ......");
// 使用 timer 添加两个任务,希望它们都在 1s 后执行
// 但由于 timer 内只有一个线程来顺序执行队列中的任务,因此『任务1』的延时,影响了『任务2』的执行
timer.schedule(task1, 1000);
timer.schedule(task2, 1000);
}
}
使用 ScheduledExecutorService 改写:
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
pool.schedule(()->{
log.info("task1");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},1, TimeUnit.SECONDS);
pool.schedule(()->{
log.info("task2");
},1, TimeUnit.SECONDS);
}
scheduleAtFixedRate 和 scheduleWithFixedDelay例子:
public static void main(String[] args) {
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
log.info("start....");
// 一开始,延时 1s,接下来,由于任务执行时间 > 间隔时间,间隔被『撑』到了 2s
pool.scheduleAtFixedRate(()->{
log.info("running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1, TimeUnit.SECONDS);
// 一开始,延时 1s,scheduleWithFixedDelay 的间隔是 上一个任务结束 <-> 延时 <-> 下一个任务开始 所以间隔都是 3s
pool.scheduleWithFixedDelay(()->{
log.info("running...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 1, 1, TimeUnit.SECONDS);
}
整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务
【8.2】正确处理执行任务异常
方法1:主动捉异常
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.submit(()->{
try {
int i = 1 / 0;
} catch (Exception e) {
log.error("error",e);
}
});
}
输出:
方法2:使用 Future
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1);
Future<Boolean> future = pool.submit(() -> {
int i = 1 / 0;
return true;
});
log.info("result:{}", future.get());
}
输出:
【8.3】 定时任务案例
@Slf4j
public class TestSchedule {
// 如何让每周四 18:00:00 定时执行任务
public static void main(String[] args) {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);
// 获取周四时间
LocalDateTime time = now.withHour(18).withMinute(0).withSecond(0).withNano(0).with(DayOfWeek.THURSDAY);
// 如果当前时间 > 本周四, 必须找到下周四
if (now.compareTo(time) > 0){
time = time.plusWeeks(1);
}
// initailDelay 代表当前时间和周四的时间差
// period 一周的间隔时间
long initailDelay = Duration.between(now, time).toMillis();
long period = 1000 * 60 * 60 * 24 * 7;
ScheduledExecutorService pool = Executors.newScheduledThreadPool(1);
pool.scheduleAtFixedRate(()->{
log.info("running......");
}, initailDelay, period, TimeUnit.MILLISECONDS);
}
}
5.3 Fork/Join
【1】 概念
Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
【2】 使用
提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)
或 RecursiveAction(没有返回值)
,例如下面定义了一个对 1~n 之间的整数求和的任务
@Slf4j
public class TestForkJoin {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
Integer result = pool.invoke(new MyTask(5));
System.out.println(result);
}
}
// 1-n之间整数的和
@Slf4j
class MyTask extends RecursiveTask<Integer>{
private int n;
public MyTask(int n) {
this.n = n;
}
@Override
public String toString() {
return "{" + n + "}";
}
@Override
protected Integer compute() {
// 终止条件
if (n == 1){
log.info("join() {}", n);
return 1;
}
MyTask t1 = new MyTask(n - 1);
t1.fork(); // 让一个线程去执行此任务
log.info("fork() {} + {}", n, t1);
int result = n + t1.join(); // 获取任务结果
log.info("fork() {} + {} = {}", n, t1, result);
return result;
}
}
改进
@Slf4j
public class TestForkJoin1 {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool(4);
Integer result = pool.invoke(new MyTask1(1,10));
System.out.println(result);
}
}
// 1-n之间整数的和
@Slf4j
class MyTask1 extends RecursiveTask<Integer>{
private int begin;
private int end;
public MyTask1(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
public String toString() {
return "{" + begin + "," + end + "}";
}
@Override
protected Integer compute() {
// 终止条件
if (begin == end){
log.info("join() {}", begin);
return begin;
}
if (end - begin == 1){
log.info("join() {} + {} = {}", begin, end, end + begin);
return end + begin;
}
int mid = (end + begin) / 2;
MyTask1 t1 = new MyTask1(begin, mid);
t1.fork(); // 让一个线程去执行此任务
MyTask1 t2 = new MyTask1(mid + 1, end);
t2.fork();
log.info("fork() {} + {} = ?", t1, t2);
int result = t1.join() + t2.join(); // 获取任务结果
log.info("fork() {} + {} = {}", t1, t2, result);
return result;
}
}
6. JUC
6.1 ReentrantLock 原理
【】