多线程基础学习笔记
# 了解多线程
# 进程与线程
进程是一个电脑上的程序,线程是进程中的执行单位
进程在 java 虚拟机上的实现是拥有许多线程与堆、方法区,线程拥有自己的栈、程序计数器、本地方法栈。即一个进程有多个线程可以实现自己的独立调度,并且这些线程又有共同的资源区域
# 并发与并行
并发:并发是指多个任务在同一时间段内交替执行。这些任务可能在不同的时间点开始和结束,但它们共享同一个处理器资源。操作系统通过调度机制在多个任务之间快速切换,使得每个任务看起来像是同时在运行
并行:并行是指多个任务同时在多个处理器或核心上执行。每个任务都有独立的处理器资源,因此可以真正地同时进行
# java 线程的实现
我们知道操作系统提供的线程分配资源需要实现才可以使用
1,内核线程实现:内核线程是由操作系统内核直接管理和调度的线程,与用户线程相比,内核线程具有更高的优先级和更强的并发能力。在 Java 中,内核线程的创建和管理是由操作系统和 Java 虚拟机共同完成的,开发者无法直接控制内核线程的创建和调度
2,用户线程实现:就是让用户来实现线程的调用规则,操作系统只负责分配资源给进程,线程的创建、销毁、同步都是由用户来决定的。用户线程的创建和调度由用户程序控制,因此需要确保在主线程退出之前,所有的用户线程都已经执行完毕或被显式地停止
而 HotSpot 的线程实现是内核线程实现,每一个线程都是直接映射到操作系统原生线程来实现的。堆可以对应内存,本地方法栈与虚拟机栈可以对应寄存器与处理器。此外,还有一些虚拟机会使用其他的实现方式
# 管程 Monitor
Monitor 直译是监视器,它并非线程,而是一种并发编程的同步机制,用于协调多个线程之间的访问和操作共享资源。它提供了一种结构化的方式来管理线程的互斥访问和条件等待,它的最终作用是让线程安全
管程指的是管理共享变量以及对共享变量的操作过程,让它们支持并发。翻译为 Java 就是管理类的成员变量和成员方法,让这个类是线程安全的
在任何时候,只有一个线程可以进入管程中的某个方法,从而避免多个线程同时访问共享数据导致的竞态条件,当一个线程试图调用管程中的某个方法时,它必须先获取管程的锁
# 协程
协程的本质是一个用户状态下的单个线程通过时分复用分出来的,说人话就是它可以在执行过程中多次暂停,并在需要时恢复执行,它的调度和切换是由程序员(用户进程)显式控制的,而非依赖操作系统的抢占式调度,它可以看作是一种更高级的函数
不管是进程还是线程,每次阻塞、切换都需要陷入系统调用,先让 CPU 跑操作系统的调度程序,然后再由调度程序决定该跑哪一个线程。而且由于抢占式调度(抢占式线程)执行顺序无法确定的特点,使用线程时需要非常小心地处理同步问题。为了避免阻塞时使用系统调用,也避免用户切换状态时的开销过大,我们写出了协程(协作式线程)
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置,协程的切换在用户态完成,切换的代价比线程从用户态到内核态的代价小很多
简单点来说就是协程中函数或者一段程序能够被挂起(说暂停其实也没啥问题),待会儿再恢复
有栈协程指每个协程会保存单独的上下文(执行栈、寄存器等),协程的唤醒和挂起就是拷贝、切换上下文;无栈协程指单个线程内所有协程都共享同一个执行栈,协程的切换就是简单的函数返回
截至目前(2025年2月27日),Java 在标准库和核心语言层面通过 Project Loom 引入了对协程的支持,具体实现为虚拟线程(Virtual Threads)。虚拟线程由 JVM 管理,而不是直接映射到操作系统线程。一个操作系统线程可以承载成千上万的虚拟线程,极大提高了并发能力。切换由 JVM 调度器控制,通常在阻塞操作(如 I/O)时自动挂起和恢复,开发者无需显式调用 yield
# 为什么要使用多线程
单处理器:io 操作与 cpu 操作不能同时运行,多线程可以提高程序运行效率 多处理器:使用单线程无法同时利用所有 cpu 服务器:需要同时响应多个用户请求
# 那如何避免和预防死锁
编写并发代码的时候需要考虑2个问题
1,线程安全 2,死锁
处理死锁一共有四种方法,又分为预防死锁的方法和避免死锁的方法
预防死锁的方法(也是解决哲学家进餐问题的方法):
1,一次申请所有要使用的资源 2,申请不到资源时,可以放弃已有资源 3,让资源按序获取
避免死锁的方法:我们启动一个预处理线程,在每次分配资源之前,计算这个资源分配给这个线程会不会出现死锁的情况
# 线程安全的分类
什么是线程安全?就是多个线程在操作同一个共享对象的时候可能会发生一些不正确的结果,对共享变量的修改是产生线程安全问题的重点,而有些操作就算访问了共享变量也不会造成任何线程安全问题,java 对共享变量的访问方式一共有以下几类:
1,不可变:被 final 修饰的变量,只能进行读取,不能进行更新,自然是线程安全的 2,绝对线程安全:不管运行时环境如何,调用者都不需要任何的额外同步操作,达成这个条件是非常困难的 3,相对线程安全:通常意义上的线程安全,java 中提供的大部分线程安全的类都是相对线程安全,通过一些同步操作配合这些类实现线程安全,比如 hashtable、vector 等 4,线程兼容:可以使用同步手段保证在并发环境正常使用,比如 hashmap 5,线程对立:不管采用了什么线程安全措施都无法保证线程安全,这在 java 中很少出现
# 多线程实现方式
# 继承 Thread 类
1,构造 Thread 子类,重写 run 方法 2,创建该子类实例对象,调用 start 方法 特点:java 只能继承一个父类
# 为什么不直接使用 run 方法
执行 run 只能运行里面的代码,start 才能启动一个线程
# 实现 Runnable 接口
1,构造 Runnable 接口子类实例对象,重写 run 方法 2,创建该子类实例对象 3,调用有参的 Thread 构造方法,调用 start 方法 特点:java 可以实现多个接口,避免单继承的局限性 更好的处理共享资源的情况
# Runnable 接口为什么可以这么实现
先来看看 Thread 的 run 方法
private Runnable target;
public void run() {
if (this.target != null) {
this.target.run();
}
}
2
3
4
5
6
7
8
该方法意思是,先判断类里的 Runnable 是否为空,否则执行 target 的run方法,而创建该子类实例对象这一步就将自己创建的Runnable传入了Thread中
# 实现 Callable 接口
public interface Callable<V> {
V call() throws Exception; // 返回泛型结果 V
}
2
3
1,构造 Callable 接口子类实例对象,重写 call 方法 2,创建该子类实例对象 3,调用有参的 FutureTask 构造方法 4,调用有参的 Thread 构造方法,调用 start 方法
特点:有返回值,运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果,可声明抛出异常
代码实例:
public class Test2 {
public static void main(String[] args) throws InterruptedException, ExecutionException {
Thread1 t1 = new Thread1();
t1.start();
Thread2 t2 = new Thread2();
new Thread(t2).start();
Thread3 t3 = new Thread3();
FutureTask<Object> f = new FutureTask<>(t3);
Thread t = new Thread(f);
t.start();
System.out.println(f.get());
}
}
class Thread1 extends Thread{
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
class Thread2 implements Runnable{
public void run() {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
}
}
class Thread3 implements Callable<Object>{
public Object call() throws Exception {
// TODO Auto-generated method stub
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
return 10;
}
}
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
# FutureTask 和 Future 接口
Future 接口主要是对并发任务的执行及获取其结果的一些操作。主要有三大功能:判断并发任务是否执行完,获取并发的任务完成后的结果,取消并发执行的任务
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws ...;
}
2
3
4
5
6
7
当计算完成后,只能通过 get() 方法得到结果,get 方法会阻塞直到结果准备好了。如果想取消,那么调用 cancel() 方法。其他方法用于确定任务是正常完成还是取消了

而 FutureTask 内部自己实现了默认的 Callable 接口,但是可以传入一个新的 Callable 覆盖,FutureTask 是一个提供异步计算的结果的任务,它同时实现了 Future 和 Runnable 接口,因此它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
FutureTask 类的 get 方法,只进一次线程调用,此后直接返回以前的值;异步获取结果的同时,主线程是阻塞的。所以可以将其归为异步阻塞模式
FutureTask 底层原理为内部通过状态机(volatile int state)跟踪任务状态(NEW、COMPLETING、NORMAL、EXCEPTIONAL等),结果存储通过 outcome 字段(volatile Object)保证可见性,调用 get() 时,若任务未完成,线程会通过 LockSupport.park() 进入等待。
# 同步调用、回调、异步调用(异步回调)
同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用,像Future的get方法就是同步调用
回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口,具体做法可以是函数式接口实现
异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。回调常常是异步调用的基础
CompletableFuture可以实现异步回调
# 线程的生命周期
线程的生命周期描述了一个线程从创建到终止的整个过程,包括多个不同的状态。在 Java 中,线程的生命周期可以分为以下几个阶段。以下出自《Java 并发编程艺术》
- 新建:对象创建时,未调用 start 方法。未分配系统资源时
- 可执行:可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源,因此我们可以切割为操作系统中以下两个状态(只有6个状态是因为 java 源码中 Thread.State 切分了6个状态) -- 就绪:调用 start 方法后变为就绪状态,此时线程已经分配了系统资源,并且可以被调度执行。然而,线程还没有开始执行,只是等待系统调度 -- 运行:当线程被系统调度并开始执行时,线程进入运行状态。此时线程正在执行其任务代码
- 阻塞:满足以下两种条件之一都会出现阻塞:等待获取锁时发生阻塞,发出 IO 请求时也会发生阻塞,总之阻塞是被动的行为。在阻塞状态下,线程暂停执行,不会占用 CPU 资源
- 无期等待:使用无参的 join、wait 等方法会发生等待,等待是主动行为,会释放锁和 CPU 资源,其他的线程使用 notify、notifyAll 方法后该线程进入就绪状态
- 限期等待:使用有参的 sleep、wait、join 会定时等待,时间到也会进入就绪,会释放占有的 CPU 资源,只有 sleep 不会释放锁,其他都会释放锁
- 死亡:执行完毕,或者抛出异常,错误时线程死亡。注意,死亡后无法进入其他状态,线程是不可能复活的
关于 join、wait、sleep

请注意不要混淆操作系统线程状态和 java 线程状态。JVM 中的线程必须只能是以上6种状态的一种。操作系统的状态是以下五个

# 线程调度以及调度线程的方法
强占调度:按优先级争取 CPU 资源 定时调度:线程获得定时大小的时间片并执行
# 优先级
高优先级容易抢到CPU资源 优先级分为1到10 优先级改变使用 setPriority 方法
# 让步
线程进入就绪状态,放弃 cpu 资源 使用 yield 方法
# 休眠
使用 sleep 方法使线程休眠
# 插队(join)
现在有一个任务:在所有子线程执行完毕之后再执行主线程,该怎么做?
答案是使用 join 方法,该方法让调用这个方法的线程阻塞直到被调用的线程执行完毕。比如 A 线程插入 B 线程中,只有 A 运行结束才能运行 B
# 中断(interrupt)
Java 没有提供任何机制来安全地终止线程,但提供了中断机制,即 thread.interrupt() 方法,该方法主要作用就是让线程中断
线程中断是一种协作式的机制,并不是说调用了中断方法之后目标线程一定会立即中断,而是发送了一个中断请求给目标线程,目标线程会自行在某个取消点中断自己。它的作用其实是通知该线程应该被中断了
这种设定很有必要,为什么不能从外部强制性的终止一个线程呢?因为如果不论线程执行到何种情况都立即响应中断的话,很容易造成某些对象状态不一致的情况出现,线程应该有机会完成它正在执行的任务,并进行必要的清理工作
同时,由于需要该线程自己中断自己,它需要经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程
源码如下:
private volatile Interruptible blocker;
private final Object blockerLock = new Object();
// 核心 interrupt 方法
public void interrupt() {
// 先检查非本线程是否有权限
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 仅仅设置interrupt标志位
b.interrupt(this); // 调用如 I/O 操作定义的中断方法
return;
}
}
interrupt0();
}
// 静态方法,调用该方法调用后会清除中断状态。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
// 这个方法不会清除中断状态
public boolean isInterrupted() {
return isInterrupted(false);
}
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
# 守护线程
守护线程(Daemon Thread)是一种在后台运行的线程,它的存在并不会阻止程序的终止。与之相对的是用户线程(User Thread),用户线程是程序的主要执行线程,当所有的用户线程结束时,程序才会终止
守护线程有以下几个主要的用途:
- 后台任务:守护线程通常用于执行一些后台任务,这些任务不需要与用户交互或等待用户输入,而是在后台默默地执行。例如,垃圾回收器就是一个守护线程,负责回收不再使用的内存
- 资源管理:守护线程可以用于管理和监控一些资源,例如数据库连接池、网络连接池等。它们可以周期性地检查资源的状态,并进行相应的管理和维护
需要注意的是,守护线程在程序终止时会被强制终止,因此不能依赖于守护线程来执行关键性的任务或保证数据的完整性。它们主要用于辅助和支持用户线程的工作,提供一些额外的功能和服务
在 Java 中,可以通过 Thread 类的 setDaemon(true) 方法将一个线程设置为守护线程。默认情况下,线程是用户线程。需要注意的是,setDaemon() 方法必须在调用 start() 方法之前设置,否则会抛出 IllegalThreadStateException 异常
# 线程间通信方式
1,volatile、synchronized 关键字 + 等待通知机制
关键字 volatile 可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性,从而间接实现线程之间数据的通信
关键字 synchronized 可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性
可以通过 Java 内置的等待通知机制实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作
这种方式用来实现比较麻烦的线程间通信,比如生产者消费者问题
2,管道输入/输出流
管道输入、输出流和普通的文件输入输出流或者网络输入输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存
管道输入输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符
3,使用 Thread.join()
如果一个线程 A 执行了 thread.join 语句,其含义是当前线程 A 等待 thread 线程运行结束之后再继续往下运行,这意味着 thread 线程执行修改的数据是肯定可以被线程 A 读取到的
4,可重入锁 + condition