基础概念

进程,线程

进程

何为进程

进程是程序的一次执行过程,是程序执行的一次执行实例,是系统运行程序的基本单位,是操作系统进行资源分配和调度的基本单位,因此进程是动态的。

进程 = 程序 + 数据 + 执行状态

系统运行一个程序的本质即是一个进程从创建,运行到消亡的过程。

讲一下相对底层的内容,一个程序在磁盘上只是一串二进制指令和数据。CPU 只能执行内存中的指令。因此,运行程序的第一步是将其加载到主存RAM,而 CPU 内部有一个关键寄存器,程序计数器(PC, Program Counter),它指向下一条要执行的指令地址,差不多就是 PC → 取指令 → 解码 → 执行 → 更新PC → 循环,此时,整个机器只能运行一个程序,CPU 的 PC 寄存器始终指向该程序的指令

问题来了,如果我想同时运行两个程序怎么办?所以才引入的进程。

进程是实现多道程序设计的核心设计,多个进程可同时存在于内存中才能使得 CPU 同时运行多个程序,操作系统通过 PCB 控制每个进程能使用的资源,所以它才能动态,并发,独立,异步

在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。

在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(.exe 文件的运行)。

image-20260206204701060

线程

线程与进程相似,但线程是一个比进程更小的执行单位。线程(Thread)是进程内的一个执行流,是 CPU 调度和分派的基本单位

因为进程创建/销毁进程开销太大了,很难接受,而且进程间通信复杂,而且考虑到,同一个程序内部也需要并发,所以才引入了线程,线程 = 共享进程资源 + 独立执行上下文

与进程不同的是,同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

每个线程有自己的:

  • 线程 ID(TID)
  • 程序计数器(PC)
  • 寄存器集合
  • 栈空间(Stack) ← 存放局部变量、方法调用记录

共享所属进程的:

  • 代码段(.text)
  • 数据段(全局变量、静态变量)
  • 堆(Heap)
  • 打开的文件、网络连接等

而对于 Java 而言,Java 中的 java.lang.Thread 是对操作系统线程的封装。而 Java 程序天生就是多线程程序,我们可以通过 JMX 来看看一个普通的 Java 程序有哪些线程,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
public class MultiThread {
public static void main(String[] args) {
// 获取 Java 线程管理 MXBean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
// 遍历线程信息,仅打印线程 ID 和线程名称信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
image-20260207002229709

从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行

Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。

多线程模型是Java程序最基本的并发模型

每个 Java 线程在 JVM 中对应

  • 私有栈(Java Stack):存储局部变量、方法调用帧(Frame)
  • 程序计数器(PC Register):记录当前执行的字节码指令地址

而以下区域是所有线程共享的:

  • 堆(Heap):存放对象实例
  • 方法区(Metaspace):存放类信息、常量、静态变量

那么,线程安全问题根源很明显,就是多个线程同时修改堆中的同一个对象(如 ArrayList),若无同步机制,会导致数据不一致。

所以说,对于线程和进程的区别和比较,可以总结如下

线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。

为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢

程序计数器、虚拟机栈和本地方法栈是线程私有的,根本原因在于:它们共同构成了线程的“执行上下文”(Execution Context)。为了保证多线程环境下每个线程能独立、正确、高效地执行自己的代码路径,JVM 必须为每个线程分配独立的这三块内存区域。

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。这在 JVM 部分也有描述

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置,对于多线程场景,线程切换的本质是保存和恢复上下文,程序计数器必须私有

别忘了,程序计数器是 JVM 中唯一不会发生 OutOfMemoryError 的区域,因为其大小固定

而虚拟机栈存储 栈帧(Stack Frame),每个 Java 方法调用对应一个栈帧。栈帧包含局部变量表,操作数栈,动态链接和方法返回地址,而每个线程的方法调用链是独立的,它们私有确保局部变量能够天然线程安全,而且在多线程并发的时候避免方法调用链混乱、返回地址错误

1
2
3
4
5
6
7
8
9
10
public void threadA() {
int x = 10; // 局部变量 x
compute(x);
}

public void threadB() {
String name = "Alice"; // 局部变量 name
greet(name);
}
// 如果虚拟机栈是共享的,线程 A 的 x 和线程 B 的 name 会混在同一栈中;线程 A 调用 compute() 时压入的栈帧,可能被线程 B 的 greet() 覆盖,那就违背有序性了

本地方法栈是为 JVM 调用 本地方法 服务,JNI 调用 C/C++ 代码等,功能与虚拟机栈类似,但服务于非 Java 代码,所以也必须私有防止 Native 层状态混乱

线程隔离必须贯穿整个执行链,从 Java 字节码到 Native 代码都不能例外 。

多线程

一个进程在其执行的过程中可以产生多个线程,那么就叫多线程。

那简单说一下多线程。

那么,多线程就是指在一个程序(进程)内部,同时存在多个执行流(线程),这些线程可以并发或并行地执行任务。因为线程是操作系统能够调度的最小执行单位。

而此时,多个线程共享同一进程的内存空间,通信方便但需注意线程安全,每个线程自己也有独立的栈空间,用于存储局部变量和方法调用,注意,线程创建、切换的开销远远小于进程。

多线程 ≠ 并发 ≠ 并行。多线程是一种实现手段,它可以用来实现并发或并行。

那么,对于多线程,完全可以使用单核 CPU 实现

操作系统主要通过两种线程调度方式来管理多线程的执行:

  • 抢占式调度(Preemptive Scheduling):操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。
  • 协同式调度(Cooperative Scheduling):线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。

操作系统调度的最小任务单位其实不是进程,而是线程。常用的Windows、Linux等操作系统都采用抢占式多任务,如何调度线程完全由操作系统决定,程序自己不能决定什么时候执行,以及执行多长时间。

所以说,Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多

同一个应用程序,既可以有多个进程,也可以有多个线程,因此,实现多任务的方法,有以下几种:

image-20260207104429204

并发(Concurrency) ,并行(Parallelism)

什么是并发?为什么需要并发?

  • 并发(Concurrency):指在同一时间段内,多个任务交替执行
  • 并行(Parallelism):真正的多个任务同时执行,需要多核CPU支持

并发是指在一段时间内,系统能处理多个任务,这些任务在宏观上看起来是“同时进行”的,但在微观上是交替执行的,也就是说,它是 CPU 执行任务时类似时分复用的一个思想。

并发的核心是:“dealing with lots of things at once”(处理多件事的能力)。所以并发关注的是任务管理的能力,它不依赖多核硬件。

通常情况下,在单核 CPU 上,操作系统通过时间片轮转快速切换任务,每个任务执行一小段时间,保存其上下文,然后切换到下一个任务,这样类似时分复用的思路,用户几乎感受不到延迟,完全可视作所有任务都几乎在同时运行。

说一下是如何进行上下文切换的,操作系统为每个运行中的程序创建一个 进程控制块,学过操作系统的都知道这是 PCB,它包括PID,程序计数器,内存映射等进程需要的关键内容,当操作系统要切换进程时,它会保存当前进程的 PCB,把所有寄存器值写入内存,然后加载下一个进程的 PCB,把寄存器值恢复到 CPU,CPU 继续执行,这就是 上下文切换

而并行,是指在同一物理时刻,多个任务真正同时执行。这必须依赖多核CPU、多处理器或分布式系统。

并行的核心是:“doing lots of things at once”(完成多件事的能力)。所以并行关注的是物理上对多任务执行的效率,必须有多核硬件支持。

并行必须在多核CPU上执行,多核CPU中,每个核心独立执行一个线程/进程,所以,它无需任务切换,无上下文切换开销。是真正提升多个计算密集型任务的执行速度的处理方式。

同步,异步

  • 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
  • 异步:调用在发出之后,不用等待返回结果,该调用直接返回

“诶诶,那不对啊,我调用在发出之后,没有结果就返回,要是我需要返回值,那不白运行了”

那么,异步 ≠ 没有结果,而是结果的获取方式不同

同步调用是线性、顺序执行的,在没有得到结果之前,它会阻塞等待,直到结果返回,也就是说,后面的事情想要处理也要排队等着它,这就很恼火了,调用期间线程被占用,这是同步的问题,所以说,异步要解决这件事

异步是执行完了,走了,立即返回,不阻塞后续代码的执行,而结果会通过 回调(Callback)、Future、事件、消息队列 等机制通知,这样,调用期间线程可处理其他任务,不会等着一直,这样就很好的提高了并发能力

操作系统与编程语言如何支持异步?以 IO 和 NIO 为例子

  • 同步 I/Oread(fd, buffer) 会阻塞线程,直到数据从磁盘/网络到达。
  • 异步 I/O(如 Linux 的 io_uring、Windows 的 IOCP):
    • 调用 aio_read() 后立即返回;
    • 内核在后台完成 I/O;
    • 完成后通过 信号、回调或事件通知 应用程序。

同步与异步的本质区别在于控制流的管理方式

  • 同步调用者主动等待,结果通过函数返回值直接获取,逻辑线性但可能阻塞;
  • 异步被调用方主动通知,结果通过回调、Future 或消息机制传递,逻辑非线性但能提升并发效率。

并发编程三要素

  • 原子性:一个或者多个操作要么全部执行成功要么全部执行失败,不会出现中间状态。和事务的原子性是一样的
    • 在 Java 中,synchronized 和 Lock 是保证原子性的重要工具。
  • 有序性:程序执行顺序按照代码顺序先后执行,但是CPU可能会对指令进行重排序。导致程序执行的顺序与代码顺序不同。
    • Java 提供了 volatilesynchronized 和 内存屏障 来避免重排序问题。
  • 可见性:当多个线程访问同一个变量时,如果一个线程修改了变量,其他线程立即获取最新的值。Java 中每个线程都有自己的工作内存,当线程 A 修改了一个变量,线程 B 并不知道,因为 B 可能还在用它的旧值。这种情况下就会发生数据不一致的问题
    • 为了解决可见性问题,Java 提供了 volatile 关键字,以及 synchronizedLock 机制。

所以说,遵守上面的原则,就是遵循了多线程安全的本质需求,因为多线程的安全问题可以概括为一句话:保证并发访问的共享资源在任何时候都是一致的

Java 线程和操作系统的线程的关系

JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。但是这种线程有很多问题。

在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。所以说,现在的 Java 线程的本质其实就是操作系统的线程

用户线程和内核线程是有很大区别的

  • 用户线程:由用户空间程序管理和调度的线程,运行在用户空间
  • 内核线程:由操作系统内核管理和调度的线程,运行在内核空间,只有内核程序可以访问。

用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。

单核 CPU 上运行多个线程效率一定会高吗?也就是说,多线程一定快吗?

问题来自 Java 并发编程的艺术

单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:

  • CPU 密集型:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
  • IO 密集型:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。

在单核 CPU 上,同一时刻只能有一个线程在运行,其他线程需要等待 CPU 的时间片分配。

而我们知道,线程之前切换需要保存上下文,进行上下文切换,那么这个开销不是无形的,假如你是 CPU 密集型,你执行了20ms在一个时间片内,但是你上下文切换吧用了5ms,切换了20次,这就很鸡肋了,你的速度应该不是很好。

所以说,对于 CPU 密集型任务,那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。那么多个线程同时运行会导致频繁的线程切换,增加了系统的开销,降低了效率。

如何理解线程安全和不安全?

线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。

  • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
  • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失

因为线程共享的是进程的堆内存,而当多个线程共享同一份数据,并且至少有一个线程会对数据进行操作时,如果不采取任何保护措施,就极易产生线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
public class UnsafeCounter {
private int count = 0;

public void add() {
count++; // count = count + 1;
}

public int get() {
return count;
}
}
  • count++ 看似是一个操作,但实际上是一个“读取-修改-写入”的三步操作。在多线程环境下,可能会发生线程A刚读取完值,CPU就被线程B抢走,B也读取了同样的值并完成加1写入,随后A又用自己的旧值加1后写入,最终导致两次加法操作只生效了一次。

线程生命周期与状态管理

Java线程的6种状态

Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:

  • NEW: 初始状态,线程被创建出来但没有被调用 start()
  • RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。
  • BLOCKED:阻塞状态,需要等待锁释放。
  • WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
  • TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
  • TERMINATED:终止状态,表示该线程已经运行完毕。

线程在生命周期中并不是固定处于某一个状态,而是随着代码的执行在不同状态之间切换。

Java 线程状态变迁图

在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。因为现在大部分操作系统针对多线程并发都是时分复用,而时分式复用通常使用抢占式调度,这个时间非常少,10-20ms左右,而 READY 本来就短,线程切换的如此之快,区分这两种状态就没什么意义了

在Java程序中,一个线程对象只能调用一次start()方法启动新线程,并在新线程中执行run()方法。一旦run()方法执行完毕,线程就结束了。因此由上图可以分析线程的生命周期

  • 线程创建之后它将处于 NEW(新建) 状态

  • 调用 start() 方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。

  • 当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

  • TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  • 当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

  • 线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态

    线程终止的原因有:

    • 线程正常终止:run()方法执行到return语句返回;
    • 线程意外终止:run()方法因为未捕获的异常导致线程终止;
    • 对某个线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。

线程模型

线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种:

  1. 一对一(一个用户线程对应一个内核线程)
  2. 多对一(多个用户线程映射到一个内核线程)
  3. 多对多(多个用户线程映射到多个内核线程)
常见的三种线程模型

在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一,但是 JVM 规范里没有对此相关的任何规定,也就是说你怎么实现都可以,而且 Java 并不暴露出不同线程模型的区别,上层应用是感知不到差异的,只是性能特性略微不同

Green Threads vs Non Green Threads

Java中创建线程的三种方式

Java提供了三种主要的创建线程的方式

继承 Thread

创建一个类,继承 Thread 类,重写其中的 run() 方法,然后创建子类实例并调用其start() 方法。

1
2
3
4
5
6
7
8
9
10
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务逻辑
}
}

// 启动线程
MyThread t = new MyThread();
t.start(); // 必须是 start(),不是 run()

但是,Java 是单继承语言,继承 Thread不能再继承其他类。所以说,这种方式使用的比较少,而且根据设计模式,它违反组合优于继承的原则,而为什么必须调用 start(),而不是 run(),也说一句

  • t.run() 只是普通方法调用,不会创建新线程
  • t.start() 才会通知 JVM 创建新线程并回调 run()
image-20260207105742010

实现 Runnable 接口

实现 Runnable 接口的 run()方法,然后将 Runnable 实例作为参数传递给 Thread 构造函数。

1
2
3
4
5
6
7
8
9
10
class MyTask implements Runnable {
@Override
public void run() {
// 任务逻辑
}
}

// 创建线程并传入任务
Thread t = new Thread(new MyTask());
t.start();

以卖票为例子,多个线程共享同一个任务

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
// 1. 定义任务(Runnable)
class TicketSeller implements Runnable {
private int tickets = 10; // 共享资源

@Override
public void run() {
while (tickets > 0) {
synchronized (this) { // 简单同步,避免超卖
if (tickets > 0) {
System.out.println(
Thread.currentThread().getName() +
" 卖出第 " + tickets-- + " 张票"
);
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// 2. 主程序:多个线程执行同一任务
public class RunnableExample {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller(); // 只创建一个任务对象

// 三个窗口(线程)共享同一个售票任务
new Thread(seller, "窗口1").start();
new Thread(seller, "窗口2").start();
new Thread(seller, "窗口3").start();
}
}

Runnable 是一个任务Thread执行者,二者解耦,同一个 Runnable 实例可被多个 Thread 共享,实现资源共享,这样不仅避免了避免单继承限制,而且任务与线程分离,符合面向对象设计,而且最重要的是可配合线程池使用,所以我们基本都用这招

实现 Callable + FutureTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.concurrent.*;

class MyTask implements Callable<String> {
@Override
public String call() throws Exception {
// 任务逻辑,可返回结果、抛出异常
return "result";
}
}

// 使用 FutureTask 包装
Callable<String> task = new MyTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start();

// 获取结果(会阻塞直到完成)
String result = futureTask.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.concurrent.*;

class FibonacciTask implements Callable<Long> {
private int n;

public FibonacciTask(int n) {
this.n = n;
}

@Override
public Long call() throws Exception {
System.out.println("开始计算 fib(" + n + ")");
long result = fib(n);
Thread.sleep(2000); // 模拟耗时
System.out.println("fib(" + n + ") 计算完成");
return result;
}

private long fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}
}

public class CallableExample {
public static void main(String[] args) {
try {
// 1. 创建任务
Callable<Long> task = new FibonacciTask(40);

// 2. 包装为 FutureTask
FutureTask<Long> futureTask = new FutureTask<>(task);

// 3. 启动线程
new Thread(futureTask).start();

// 4. 主线程做其他事
System.out.println("主线程去做别的事情...");

// 5. 获取结果(会阻塞,直到计算完成)
Long result = futureTask.get(); // 阻塞等待
System.out.println("斐波那契结果: " + result);

} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
image-20260207110156192

Callable 与 Runnable 类似,但关键区别在于它有返回值,并且可以抛出异常。而futureTask.get() 会阻塞当前线程,直到任务完成,所以更推荐与线程池结合使用

线程的执行顺序和优先级

Java 中线程的执行顺序问题,这个问题看似不简单,实则涉及 线程调度、JVM 启动机制、并发不确定性 等核心概念,确实不简单

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadOrderDemo {
public static void main(String[] args) {
System.out.println("【Main】开始");

// 创建并启动子线程
new Thread(() -> {
System.out.println("【Thread-1】运行中");
}).start();

System.out.println("【Main】结束");
}
}
image-20260207112631846

它的顺序和我们想的不太一样啊,不是【Main】开始 【Thread-1】运行中 【Main】结束 吗?

因为调用 thread.start() 只是“请求” JVM 启动新线程,但不保证它立即执行;主线程和子线程的执行顺序由操作系统线程调度器决定,具有不确定性。

main方法本身就是在一个线程中运行的,所以 System.out.println("【Main】...") 是在 main 线程 中打印的,start()向 JVM 请求创建一个新的操作系统线程,在新线程中异步调用 run() 方法,但 start() 方法本身会立即返回,不会等待新线程执行,从thread线程开始运行以后,两个线程就开始同时运行了,并且由操作系统调度,程序本身无法确定线程的调度顺序,我们控制不了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
System.out.println("【Main】启动");

for (int i = 1; i <= 5; i++) {
final int id = i;
new Thread(() -> {
System.out.println("【Thread-" + id + "】执行");
}).start();
}

System.out.println("【Main】结束");
}
}
image-20260207112840066

线程启动顺序 ≠ 执行顺序

而且可以对线程设定优先级,设定优先级的方法是:

1
Thread.setPriority(int n) // 1~10, 默认值5

JVM自动把1(低)~10(高)的优先级映射到操作系统实际优先级上(不同操作系统有不同的优先级数量)。优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行

中断线程

中断是一种取消机制,不是杀死进程然后给他复活

假如,我在下载,我发现下载到了贪玩蓝月,我点击取消下载,浏览器程序必须安全、及时地停止正在运行的下载线程,否则就要我是渣渣辉了

那么,Java 没有提供一个良好的强制杀死线程的方法Thread.stop() 已废弃,会导致数据不一致、资源泄漏,完全不用啊

所以,Java 采用了一种 协作式中断 机制:

  • 一个线程不能直接杀死另一个线程,只能请求它停止,由被请求的线程自己决定何时、如何退出。

这就是 线程中断

中断一个线程非常简单,只需要在其他线程中对目标线程调用interrupt()方法,目标线程需要反复检测自身状态是否是interrupted状态,如果是,就立刻结束运行。

  • 调用 t.interrupt(),给线程 t 打上一个“中断标记”,设置其内部的 interrupted 标志为 true

    image-20260207114458397
  • 线程 t 自己在代码中定期检查这个标记,如果发现被中断,就主动退出 run() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 中断线程
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1); // 暂停1毫秒
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}

class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}

那么,main线程通过调用t.interrupt()方法中断t线程,但是要注意,interrupt()方法仅仅向t线程发出了“中断请求”,至于t线程是否能立刻响应,是怎么样响应,响应后多久退出run()方法,要看具体代码。而t线程的while循环会检测isInterrupted(),所以上述代码能正确响应interrupt()请求,使得自身立刻结束运行run()方法。

还有一种中断线程的方式就是使用 volatile 布尔标志位,适用于无阻塞的计算任务

1
2
3
4
5
6
7
8
9
10
class HelloThread extends Thread {
public volatile boolean running = true; // 必须加 volatile!
public void run() {
while (running) { // 检查自定义标志
// ...
}
}
}
// main 中:
t.running = false; // 请求停止

为什么必须加 volatile

volatile是声明一个字段的,确保所有线程看到这个字段的值是一致的,保证可见性,对于写操作,它立即刷新到主内存,对于读操作,每次都从主内存读取最新值。

很明显,如果没有 volatilemain 线程修改了 running = false,但 HelloThread 可能一直读取自己工作内存中的旧值 true,导致 死循环,无法退出

在 x86 架构上,即使不加 volatile 也可能看起来正常,但在 ARM 或高并发下,一定会出问题

如果线程正在执行 Thread.sleep(), Object.wait(), Thread.join()阻塞方法,它无法“主动检查” isInterrupted(),怎么办?

存在这样的一个异常,InterruptedException 表示线程在执行可中断的阻塞操作,它的异常对象的消息为空,阻塞方法收到中断请求的时候就会抛出InterruptedException异常,那么,当线程在阻塞时被 interrupt(),会抛出InterruptedException异常,JVM 会立即唤醒它,并抛出 InterruptedException,同时清除中断标志,这样就实现了中断线程阻塞方法

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
class MyThread extends Thread {
@Override
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}

class HelloThread extends Thread {
@Override
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
}
}
}

public class Test {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
image-20260207115629705
  • main线程通过调用t.interrupt()从而通知t线程中断,而此时t线程正位于hello.join()的等待中,此方法会立刻结束等待并抛出InterruptedException。由于我们在t线程中捕获了InterruptedException,因此,就可以准备结束该线程。在t线程结束前,对hello线程也进行了interrupt()调用通知其中断。

中断一个管理多个子线程的主线程时,必须负责中断所有子线程,否则它们会变成孤儿线程,导致 JVM 无法退出!

守护线程

Java程序入口就是由 JVM 启动main线程,main线程又可以启动其他线程。当所有线程都运行结束时,JVM退出,进程结束。

如果有一个线程没有退出,JVM进程就不会退出。所以,必须保证所有线程都能及时结束,才能结束 JVM 对这个类的运行

但是问题来了,我们通常会使用定时任务,或者 JVM 中垃圾回收线程,这种它是不会允许它完成的,要不然我们还怎么定时触发,怎么随时GC,所以说有时候线程的目的就是无限循环

1
2
3
4
5
6
7
8
9
10
11
12
13
class TimerThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println(LocalTime.now());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
}
}

但如果这个线程不结束,JVM进程就无法结束。问题是,由谁负责结束这个线程?JVM 的大手?

很明显,如果不去管,这类线程经常没有负责人来负责结束它们。但是,当其他线程结束时,JVM进程又必须要结束,不能让它挡着,怎么办?

首先,Java 中的线程主要分为两类:用户线程(User Thread)和守护线程(Daemon Thread)。Java语言中无论是线程还是线程池,默认都是用户线程,因此用户线程也被称为普通线程。而守护线程通常情况下叫后台线程。

对于上述问题,答案是使用守护线程

  • 守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

当线程中的用户线程都执行结束后,守护线程也会跟随结束,守护线程具有自动结束生命周期的特性,因此,JVM退出时,不必关心守护线程是否已结束。

对于上述的定时任务,其他业务的线程停止了,它没有必要继续定时触发,把它设置成守护进程就可以

如何创建守护线程呢?方法和普通线程一样,只是在调用start()方法前,调用setDaemon(true)把该线程标记为守护线程:

1
2
3
Thread t = new MyThread();
t.setDaemon(true);
t.start();

注意setDaemon(true) 必须写在start()方法前面;

不能把正在运行中的线程设置为守护线程

守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。

而且守护线程中创建的子线程,默认情况下也属于守护线程。

image-20260207121247633

而且线程可以通过isDaemon()方法来判断当前线程或指定线程是否为守护线程

1
2
3
Thread thread = new Thread(() -> {
log.info("线程:{},是否是守护线程:{}", Thread.currentThread().getName(), Thread.currentThread().isDaemon());
}, "子线程");

同样。守护进程也有优先级,默认也是 5,默认守护线程和用户线程是同一个优先级

如果设置线程池为守护线程,则需要将线程池中每个线程都设置为守护线程

1
2
3
4
5
6
7
8
9
ExecutorService threadPool = Executors.newFixedThreadPool(10, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
// 设置线程为守护线程
t.setDaemon(true);
return t;
}
});
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 static void main(String[] args) {
System.out.println("当前线程:" + Thread.currentThread().getName());
//创建一个用户线程,一直运行
Thread thread = new Thread(() -> {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println("当前线程:" + Thread.currentThread().getName() +"正在运行");
} catch (InterruptedException e) {
System.out.println("当前线程:" + Thread.currentThread().getName() + ",休眠异常:" + e);
}
}
}, "子线程");

//启动线程
thread.start();

//主线程休眠2s
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("主线程:" + Thread.currentThread().getName() +",休眠2s");
} catch (InterruptedException e) {
System.out.println("主线程:" + Thread.currentThread().getName() + "休眠异常:"+ e );
}

//主线程结束,打印信息
System.out.println("主线程:" + Thread.currentThread().getName() +",结束运行");
}
image-20260207120721358
  • 可以看到主线程main线程启动后,首先会去创建一个用户线程,这个用户线程是死循环,当主线程运行结束后,JVM也不会退出,因为这个子线程还在运行

  • 改成守护线程

    image-20260207120909597
    image-20260207120919739

    由上述运行结果可知,当主线运行结束后,JVM也结束了运行,被设置为守护线程的子线程也结束了运行。