多线程基础
多线程基础
参考视频:https://www.bilibili.com/video/BV16J411h7Rd?p=17
1.进程和线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在 指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
线程
- 一个进程之内可以分为一到多个线程。
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作 为线程的容器
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂 同一台计算机的进程通信称为 IPC(Inter-process communication) 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
2.并行和并发
并发:
单核 cpu 下,线程实际还是 串行执行 的。操作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows 下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感 觉是 同时运行的 。
总结为一句话就是: 微观串行,宏观并行 , 一般会将这种 线程轮流使用 CPU 的做法称为并发, concurrent


并行
多核 cpu下,每个 核(core) 都可以调度运行线程,这时候线程可以是并行的


例子:
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一 个人用锅时,另一个人就得等待)
3.应用
异步调用
以调用方角度来讲,
- 如果 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
设计
多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停...
如果能使用异步的话,尽量使用异步,避免主线程阻塞,提高效率
结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- 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.单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
-
2.多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任 务都能拆分(参考后文的【阿姆达尔定律】) 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
-
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
4.创建线程的方法
1.直接使用Thread类
调用start方法执行启动线程(start方法使得线程进行就绪状态,执行需要通过底层的cpu调度)
public class Demo1 {
public static void main(String[] args) {
//创建线程对象(匿名内部类的方式)
Thread thread1 = new Thread("thread1"){
//线程执行逻辑
@Override
public void run() {
System.out.println("thread1-----");
}
};
//开启线程
thread1.start();
}
}
2.Ruanble配合thread
线程和任务分开, runnable放执行任务,thread执行的就是runnable的run方法
public class Demo2 {
public static void main(String[] args) {
// Runnable runnable = new Runnable() {
// public void run() {
// System.out.println(Thread.currentThread().getName());
// System.out.println("aaaaaaaa");
// }
// };
//lambda表达式写法
Runnable runnable = ()->{
System.out.println("aaa");
};
Thread thread = new Thread(runnable,"t2");
thread.start();
}
}
thread和runnable对比:
- runnable将任务和线程分开,更加灵活,同一个runnable任务可以被多个线程使用
- 最后都是通过thread来开启线程的
3.FutureTask结合Thread
FutureTask实际上也是间接实现了Runable接口

可以获得线程执行的返回结果(内部callable的执行结果)
public class Demo3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("running...");
Thread.sleep(1000);
return 100;
}
});
Thread thread = new Thread(futureTask,"t3");
thread.start();
//获取执行结果
Integer res = futureTask.get();
System.out.println(res);
}
}
观察多个线程同时执行
public class Test {
public static void main(String[] args) {
new Thread(()->{
while (true){
System.out.println(100);
}
}).start();
new Thread(()->{
while (true){
System.out.println(200);
}
}).start();
}
}
结论:
多个线程同时开启,不能保证执行顺序,是交替执行的,由底层cpu决定
程序员没法控制
5.查看进程,线程命令
windows
#任务管理器可以查看进程和线程数,也可以用来杀死进程
#tasklist 查看进程
tasklist | findstr java #管道符筛选
#taskkill 杀死进程
taskkill /F /PID 13332 #杀死进程id为13332的进程
Linux
ps -ef #查看所有进程
ps -ef | grep java #管道符筛选
ps -fT -p <PID> #查看某个进程(PID)的所有线程
kill 杀死进程
kill -9 进程id #强制杀死
top #动态查看进程信息,按大写 H 切换是否显示线程
top -H -p <PID> #查看某个进程(PID)的所有线程
Java(jdk带的)
jps #命令查看所有 Java 进程
jstack <PID> #查看某个 Java 进程(PID)的所有线程状态
jconsole #来查看某个 Java 进程中线程的运行情况(图形界面)
使用jconsole图形化界面:

jconsole 远程监控配置
远程加上以上配置运行Java类,
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类
例如:
java -Djava.rmi.server.hostname=47.107.93.172 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=3344 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false Test
jcosole连接远程:


6.原理之线程运行
栈与栈帧
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 频繁发生会影响性能
7.线程常见方法
run和start
调用run
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("t1");
}, "t1");
thread.run();
System.out.println("main");
}
}
程序仍在 main 线程运行, FileReader.read() 方法调用还是同步的
调用start
thread.start();
程序在 t1 线程运行, System.out.println("t1")调用是异步的
结论
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,run 中的代码在新的线程t1中执行
sleep和yield
都是Thread中的静态native方法
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性 ,底层还是Thread.sleep
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int count = 1;
while (true) {
Thread.yield();//让出去执行权
System.out.println(" >>>t1。。。。:" + count++);
}
});
Thread t2 = new Thread(() -> {
int count = 1;
while (true) {
System.out.println(">>>t2。。。。:" + count++);
}
});
t1.start();
t2.start();
}
}
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- java中线程优先级为1到10,数字越大越高,默认为5
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int count = 1;
while (true) {
System.out.println(" >>>t1。。。。:" + count++);
}
});
Thread t2 = new Thread(() -> {
int count = 1;
while (true) {
System.out.println(">>>t2。。。。:" + count++);
}
});
//设置优先级
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);
t1.start();
t2.start();
}
}
案例:sleep限制cpu的消耗
sleep 实现
在没有利用 cpu 来计算时,不要让 while(true) 空转浪费 cpu,这时可以使用 yield 或 sleep 来让出 cpu 的使用权 给其他程序
在1个核心的机器上测试效果更明显
public class Test3 {
public static void main(String[] args) {
while (true){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
不加sleep几乎占满整个cpu:

加了sleep后占用少量cpu

join
等待线程(调用join方法的线程)运行结束
public class Test4 {
private static int a = 10;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 100;
}, "t1");
t1.start();
t1.join();//主线程等待t1运行完 再打印
System.out.println(a);
}
}
- join可以使线程按照顺序执行(异步变成同步)
- join底层原理就是wait方法
interrupt
打断sleep,wait ,join的线程
sleep,wait ,join方法被interrupt方法打断后,会清空打断状态,打断标记(thread.isInterrupted())会重新置为false
public class Test5 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
//让thread先睡眠再打断
Thread.sleep(1000);
thread.interrupt();
System.out.println("打断标记:"+thread.isInterrupted());//false
}
}
打断正常运行的线程
打断正常运行的线程, 不会清空打断状态 为true
通过打断标志来判断是否停止运行线程
public class Test6 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
System.out.println("被打断了 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
打断park线程
LockSupport.park()使用 使当前线程执行停止到这个地方,不会停止程序。。
public class Test8 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println("park");
LockSupport.park();//当前线程执行停止到这个地方。。
System.out.println("unpark");
},"t1");
t1.start();
}
}
打断 park 线程, 不会清空打断状态 为true
public class Test8 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("park");
LockSupport.park();//当前线程执行停止到这个地方。。
System.out.println(Thread.currentThread().isInterrupted());//true
System.out.println("unpark");
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
如果打断标记已经是 true, 则 park 会失效
public class Test8 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("park");
LockSupport.park();
System.out.println(Thread.currentThread().isInterrupted());//true
System.out.println("unpark");
//如果打断标记已经是 true, 则 park 会失效
LockSupport.park();
System.out.println(111);
System.out.println(111);
System.out.println(111);
System.out.println(111);
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
提示:可以使用 Thread.interrupted() 清除打断状态(设置为false),使lock重新生效
public class Test8 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("park");
LockSupport.park();
System.out.println(Thread.currentThread().isInterrupted());//true
System.out.println("unpark");
//如果打断标记已经是 true, 则 park 会失效
Thread.interrupted();//清除打断状态 设置为false
System.out.println(Thread.currentThread().isInterrupted());//false
LockSupport.park();
System.out.println(111);
System.out.println(111);
System.out.println(111);
System.out.println(111);
},"t1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
终止模式之两阶段终止模式
Two Phase Termination
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
错误思路
- 使用线程对象的 stop() 方法停止线程
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁, 其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程
- 目的仅是停止一个线程,但这种做法会让整个程序都停止(有点小题大做)
两阶段终止模式
前辈们经过认真对比分析,已经总结出了一套成熟的方案,叫做两阶段终止模式。
顾名思义,就是将终止过程分成两个阶段,其中第一个阶段主要是线程 T1 向线程 T2发送终止指令,而第二阶段则是线程 T2响应终止指令。使得t2能够优雅的停止,不至于影响整体程序

实现方式1:利用 isInterrupted
interrupt 可以打断正在执行的线程,无论这个线程是在 sleep,wait,还是正常运行
需要考虑到 sleep,wait被interrupt后打断标志被清空(为false)
/**
* @author
* @date 2021/12/26 14:07
*/
public class Test7 {
public static void main(String[] args) throws InterruptedException {
TPTInterrupt tptInterrupt = new TPTInterrupt();
tptInterrupt.start();
Thread.sleep(3000);
tptInterrupt.stop();
}
}
class TPTInterrupt {
private Thread thread;
//开启监控线程
public void start() {
thread = new Thread(() -> {
while (true) {
//获取打断标记
Thread currentThread = Thread.currentThread();
if (currentThread.isInterrupted()){
System.out.println("料理后事");
break;
}
try {
Thread.sleep(1000);
System.out.println("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
//重新打断thread 是打断标志为true
currentThread.interrupt();
}
}
});
thread.start();
}
//停止监控线程
public void stop() {
thread.interrupt();
}
}
不推荐的方法
还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

8.主线程和守护线程
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。
有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
我们自己创建的线程默认为非守护线程
public class Test9 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (true){
System.out.println("啦啦啦啦");
}
},"t1");
t1.setDaemon(true);//设置t1为守护线程
t1.start();
System.out.println("main");
}
}
注意
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等 待它们处理完当前请求
9.线程状态
5种状态
这是从 操作系统 层面来描述的

- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入 【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】 ,与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑 调度它们
- 【终止状态】表示线程内容已经执行完毕,生命周期已经结束,不会再转换为其它状态
6种状态
这是从 Java API 层面来描述的 根据 Thread.State 枚举,分为六种状态

- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述
- TERMINATED 当线程代码运行结束
案例:六种状态演示
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
System.out.println("t1.....");
});
Thread t2 = new Thread(()->{
while (true){
}
});
t2.start();
Thread t3 = new Thread(()->{
System.out.println("t3.....");
});
t3.start();
Thread t4 = new Thread(()->{
synchronized (Test1.class){
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t4.....");
}
});
t4.start();
Thread t5 = new Thread(()->{
try {
t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t5.....");
});
t5.start();
Thread t6 = new Thread(()->{
synchronized (Test1.class){
System.out.println("t6....");//拿不到锁就会阻塞
}
});
t6.start();
System.out.println(t1.getState());//NEW
System.out.println(t2.getState());//RUNNABLE
System.out.println(t3.getState());//TERMINATED
Thread.sleep(1000);
System.out.println(t4.getState());//TIMED_WAITING
System.out.println(t5.getState());//WAITING
System.out.println(t6.getState());//BLOCKED
}
}
10.案例:统筹(烧水泡茶)
比如,想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么 办?
- 办法甲:洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开 了,泡茶喝。
- 办法乙:先做好一些准备工作,洗水壶,洗茶壶茶杯,拿茶叶;一切就绪,灌水烧水;坐待水开了,泡 茶喝。
- 办法丙:洗净水壶,灌上凉水,放在火上,坐待水开;水开了之后,急急忙忙找茶叶,洗茶壶茶杯,泡 茶喝。
哪一种办法省时间?我们能一眼看出,第一种办法好,后两种办法都窝了工。
水壶不洗,不能烧开水,因而洗水壶是烧开水的前提。没开水、没茶叶、不洗茶壶茶杯,就不能泡茶,因而 这些又是泡茶的前提。它们的相互关系,可以用下边的箭头图来表示:

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

看来这是“小题大做”,但在工作环节太多的时候,这样做就非常必要了。
解法1实现:
public class Test1 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
String name = Thread.currentThread().getName();
try {
Thread.sleep(1000);
System.out.println(name+":洗水壶");
Thread.sleep(5000);
System.out.println(name+":烧开水");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"老王");
Thread t2 = new Thread(()->{
String name = Thread.currentThread().getName();
try {
Thread.sleep(1000);
System.out.println(name+":洗茶壶");
Thread.sleep(2000);
System.out.println(name+":洗茶杯");
Thread.sleep(1000);
System.out.println(name+":拿茶叶");
//等老王烧完开水才能泡茶
t1.join();
System.out.println(name+":泡茶");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"小王");
t1.start();
t2.start();
}
}
解法1缺点:
- 上面模拟的是小王等老王的水烧开了,小王泡茶,如果反过来要实现老王等小王的茶叶拿来了,老王泡茶 呢?代码最好能适应两种情况
- 上面的两个线程其实是各执行各的,如果要模拟老王把水壶交给小王泡茶,或模拟小王把茶叶交给老王泡茶 呢
11.总结:
本章的重点在于掌握
-
线程创建
-
线程重要 api,如 start,run,sleep,join,interrupt 等
-
线程状态
-
应用方面
-
异步调用:主线程执行期间,其它线程异步执行耗时操作
-
提高效率:并行计算,缩短运算时间
-
同步等待:join
-
统筹规划:合理使用线程,得到最优效果
-
原理方面
- 线程运行流程:栈、栈帧、上下文切换、程序计数器
- Thread 两种创建方式 的源码
-
模式方面
- 终止模式之两阶段终止

浙公网安备 33010602011771号