前言
上一篇已经把计算机启动、Kernel、用户态、内核态和 System Call 串了起来。
操作系统真正开始运行以后,下一个最自然的问题就是:
操作系统到底在管理谁?CPU 又是在调度谁?
我们平时会同时听到很多词:
程序
进程
线程
Linux Task
Java Thread
Platform Thread
Fiber
Coroutine
Virtual Thread
Carrier Thread
它们看起来都和“让代码跑起来”有关,但如果只背一句:
进程是资源分配的基本单位,线程是 CPU 调度的基本单位。
很多细节依然解释不了:
- 一个
.jar文件还没运行时,算进程吗? - 同一个程序为什么可以启动多个进程?
- 一个进程里的线程到底共享什么,又独立拥有什么?
- Linux 为什么经常不刻意区分“进程”和“线程”,而统一叫
task? - Java 的
Thread到底是不是 Linux 线程? - 为什么传统 Java 线程通常被叫作“重量级线程”?
- 以前常说的 Fiber、Coroutine 和今天 Java 的 Virtual Thread 是不是一回事?
- Virtual Thread 是不是比普通线程执行得更快?
- 为什么它特别适合高并发 I/O,却不适合拿来提升纯 CPU 计算速度?
这篇文章不再把这些概念分开背,而是从一段程序真正运行起来开始,把它们放到同一条执行链上。
一、程序、进程和线程是什么关系?
1. 程序为什么不等于进程?
程序首先是一份静态内容。
它可能是:
- 本地 ELF 可执行文件;
- Windows PE 文件;
- Shell / Python 脚本;
- Java 的
.class/.jar; - 以及程序运行时需要的资源和配置。
例如磁盘上有:
app.jar
只要你没有启动它,它只是存储设备中的一组字节。
它不会:
- 占用 CPU 执行;
- 拥有正在运行的线程;
- 拥有某个运行时 PID;
- 拥有当前进程的虚拟地址空间。
当执行:
java -jar app.jar
操作系统先启动 java 这个可执行程序,创建对应的运行实例;HotSpot 再加载你的 Class 和字节码。
这时候才进入“进程”的世界。
2. 进程是程序的一次运行实例
进程可以先理解成:
一段程序被操作系统真正运行起来以后形成的运行实例,以及与这次运行关联的资源和执行上下文。
一个进程通常涉及:
- 虚拟地址空间;
- 已映射的代码和数据;
- 打开的文件描述符;
- 信号处理状态;
- 线程集合;
- 身份和权限;
- 内核用于跟踪它的数据结构。
同一个程序完全可以产生多个进程。
例如:
同一个 server 可执行文件
│
├── Process A
├── Process B
└── Process C
它们运行的是相同代码,但拥有不同的运行时身份和资源环境。
所以:
Program = 静态代码/文件
Process = 某次运行实例
两者不是同一个维度。
3. 线程是进程中的执行流
一个进程不一定只有一条执行路径。
例如一个服务进程内部同时可能存在:
请求处理
GC
JIT 编译
定时任务
日志写入
网络事件处理
这些执行活动可以由不同线程承担。
可以先画成:
同一进程中的线程通常:
共享:
- 进程的虚拟地址空间;
- Heap;
- 静态数据;
- 很多进程级资源。
同时每个线程又需要保存自己的:
- 执行栈;
- 寄存器上下文;
- 指令执行位置;
- 调度状态;
- Thread Local 状态等。
因此比“线程没有自己的内存”更准确的说法是:
线程共享进程地址空间,但仍然拥有自己执行所必需的私有上下文和栈。
二、Linux 为什么把进程和线程都叫 Task?
学到这里会出现一个很有意思的问题。
从应用开发角度,我们一直在区分:
Process
Thread
但进入 Linux Kernel 后,经常会看到另一个词:
Task
1. Linux 的核心对象是 task_struct
Linux 内核使用 struct task_struct 描述可被管理和调度的 Task。
这也是理解 Linux 线程模型最重要的一步:
在 Linux 内核眼里,线程和进程并不是两套完全不同的调度对象。它们都属于 Task。
可以抽象成:
这里的继承关系只是为了表达概念,Linux 源码中并不存在 ProcessTask 和 ThreadTask 这两个 C++ 类。
很多教材会把操作系统描述进程的数据结构统称为:
PCB
Process Control Block
这个概念可以帮助理解,但在 Linux 源码里真正会看到的是:
struct task_struct
因此写 Linux 文章时,与其一直说“Linux 的 PCB”,不如直接把 task_struct 这个真实名字记住。
2. Linux 怎么区分“一个进程”和“同进程中的线程”?
关键不在于:
线程是不是一种完全不同的数据结构。
而在于:
这些 Task 共享了哪些资源,以及它们是否属于同一个 Thread Group。
Linux 中同一线程组内的线程拥有不同的 TID,但共享同一个 TGID。
可以先建立:
TGID ≈ 用户通常理解的 Process ID
TID = 内核中具体 Task / Thread 的 ID
例如:
这也是为什么 Linux 经常给人一种感觉:
“线程就是共享资源的进程。”
这个方向是对的,但更精确的说法是:
Linux 使用统一的 Task 模型;所谓线程,是一组通过特定共享关系组合在同一个 Thread Group 中的 Task。
3. clone 为什么是理解 Linux 线程的关键?
Linux 的 clone() / clone3() 提供了非常细粒度的控制:
创建新 Task 时,可以决定它与调用方共享什么。
例如:
CLONE_VM → 共享虚拟地址空间
CLONE_FILES → 共享文件描述符表
CLONE_SIGHAND → 共享信号处理配置
CLONE_THREAD → 加入同一个 Thread Group
于是:
这比“Linux 用一种特殊的轻量级进程实现线程”更接近真实内核模型。
三、Java Thread 是怎么落到 Linux 上的?
1. Platform Thread 为什么叫平台线程?
从 Java 21 开始,java.lang.Thread 明确分成两类:
Platform Thread
Virtual Thread
传统 Java 开发里使用了二十多年的:
new Thread(task).start();
默认创建的是 Platform Thread。
在主流 HotSpot + Linux 环境下,可以把它理解成:
也就是典型的 1:1 模型:
1 个 Java Platform Thread
↕
1 个 OS Thread
Java 并不是自己直接决定这个线程什么时候占用 CPU。
真正的 CPU 时间仍然由操作系统调度器安排。
2. Thread.start() 之后为什么需要操作系统参与?
执行:
Thread t = new Thread(task);
t.start();
并不是简单地在 Java Heap 里多创建一个对象。
Thread 对象当然存在于 JVM 管理的 Java 世界中,但 Platform Thread 真正开始并发执行时,还需要底层创建对应的 Native / OS Thread。
所以逻辑上会经历:
这里再次说明:
Java Thread 是语言 / JVM 层抽象,OS Thread 是操作系统层执行实体,两者不能混成一个概念。
四、为什么传统线程比较“贵”?
“Platform Thread 很重”这句话经常被解释成:
一个线程固定占 1MB,所以机器只能启动一万个线程。
这种说法太绝对。
1. 线程成本不只有 Stack
一个 OS Thread 会带来多种成本:
- Native Stack 虚拟地址空间;
- Kernel Task 数据结构;
- 调度状态;
- TLS;
- Guard Page;
- 线程相关 Runtime 数据;
- 大量线程产生的调度和 Context Switch 成本。
线程栈大小还受到:
- JVM 参数;
- libc / pthread 默认值;
- OS 限制;
- 架构;
- 容器限制;
等因素影响。
所以:
Platform Thread = 固定 1MB
不是 Java 规范。
同样:
OS Thread 最多只能 10000 个
也不是固定规则。
真正上限来自多个资源限制共同作用。
2. 大量线程为什么会带来调度压力?
假设机器只有 8 个可运行 CPU Core,却创建 100000 个 Runnable Platform Thread。
真正同时执行的依然只有少数线程。
剩下的需要:
Runnable Queue
↓
Scheduler 选择
↓
切换执行上下文
↓
运行一段
↓
再次切换
如果大量线程都在争 CPU,那么增加线程数量不会凭空增加算力,反而可能带来:
- Run Queue 更长;
- Cache Locality 下降;
- Context Switch 增多;
- Stack / Kernel Resource 增加;
- 延迟抖动。
所以线程数量从来不是:
越多越快。
五、从 Fiber 到 Java Virtual Thread
旧的 Java 并发资料经常会讨论:
Fiber
Coroutine
Quasar
Project Loom
如果只看早期资料,很容易以为 Java 仍然没有真正的用户态轻量线程。
这个结论已经过时了。
1. Quasar 更适合当作历史背景
在 Java 原生 Virtual Thread 出现以前,Quasar 等第三方项目尝试在 JVM 上实现 Fiber / Continuation 风格的轻量执行单元。
它们在理解历史演进时很有价值,但今天再写 Java 新项目时,讨论轻量线程首先应该看:
JDK Virtual Thread
而不是把 Quasar 当成主方案。
2. Java 21 正式交付 Virtual Threads
JDK 21 的 JEP 444 正式把 Virtual Threads 交付到 Java Platform。
现在可以直接写:
Thread.startVirtualThread(() -> {
System.out.println("hello virtual thread");
});
也可以:
Thread.ofVirtual().start(task);
或者:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(task1);
executor.submit(task2);
}
注意它仍然是:
java.lang.Thread
并不是 Java 另外发明了一套完全不兼容的 Fiber API。
3. Virtual Thread 不是“线程里面再切小线程”
这是很常见的直觉模型,但不够准确。
Virtual Thread 真正的核心是:
JDK 自己调度大量 Virtual Thread,并在需要真正运行 Java 代码时,把它们 Mount 到少量 Platform Thread 上。
这些 Platform Thread 被称为:
Carrier Thread
关系更接近:
也就是典型的:
M 个 Virtual Thread
↓
N 个 Platform / Carrier Thread
↓
N 个 OS Thread
这是 M:N 调度。
六、Virtual Thread 为什么特别适合 I/O?
1. Platform Thread 阻塞时发生什么?
传统线程执行:
socket.read(...);
如果数据暂时没到,Platform Thread 可能进入等待。
在 Thread-Per-Request 模型中,如果有大量请求都阻塞在:
数据库
HTTP
Socket
文件 I/O
就可能需要很多 Platform Thread 来维持并发请求数量。
2. Virtual Thread 阻塞时可以卸载 Carrier
Virtual Thread 的关键能力是:
于是:
业务上仍然可以写阻塞式代码
但:
等待 I/O 的 Virtual Thread
不必一直占着一个 OS Thread
这就是它提升高并发吞吐量的核心。
3. Virtual Thread 不是更快的 CPU 线程
假设任务只是:
for (;;) {
做纯计算();
}
Virtual Thread 仍然必须 Mount 到 Carrier Thread,最后依然需要真实 CPU Core 执行。
如果机器只有 8 个 Core:
100000 个 Virtual Thread
不会变出:
100000 个 CPU Core
所以 Virtual Thread 的核心价值是:
Scale,不是让单个计算任务跑得更快。
它尤其适合:
- 高并发请求;
- 大量网络等待;
- JDBC / HTTP / RPC 等阻塞式 I/O;
- Thread-Per-Task / Thread-Per-Request 风格。
而长时间 CPU-Bound 计算不会因为换成 Virtual Thread 自动加速。
七、实验:5000 个阻塞任务差多少?
下面做一个很简单的实验。
实验目的不是证明:
Virtual Thread 的 CPU 执行速度比 Platform Thread 快。
而是验证:
大量任务都在等待时,Virtual Thread 可以用更少的底层线程维持更高并发度。
本文实验环境:
OpenJDK 21.0.11
Linux x86_64
代码:
import java.util.concurrent.*;
public class VirtualThreadDemo {
static final int TASKS = 5000;
static final long SLEEP_MS = 100;
static long run(ExecutorService executor) throws Exception {
long start = System.nanoTime();
try (executor) {
for (int i = 0; i < TASKS; i++) {
executor.submit(() -> {
try {
Thread.sleep(SLEEP_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
return TimeUnit.NANOSECONDS.toMillis(
System.nanoTime() - start
);
}
public static void main(String[] args) throws Exception {
long platform = run(
Executors.newFixedThreadPool(200)
);
long virtual = run(
Executors.newVirtualThreadPerTaskExecutor()
);
System.out.println("platform(200) = " + platform + " ms");
System.out.println("virtual = " + virtual + " ms");
}
}
编译运行:
javac VirtualThreadDemo.java
java VirtualThreadDemo
本文实际运行一次得到:
platform(200) = 2525 ms
virtual = 131 ms
为什么固定 200 个 Platform Thread 大约需要 2.5 秒?
因为 5000 个任务,每个都等待约 100ms:
5000 / 200 = 25 批
25 × 100ms ≈ 2500ms
而 Virtual Thread 可以让大量“正在等待”的任务暂时卸载 Carrier,因此大量 sleep 可以同时处于等待状态。
但这里一定要注意:
这个实验验证的是阻塞型任务的并发扩展能力,不是 CPU 运算性能。
换成 CPU-Bound 计算后,结果完全可能不同。
八、Virtual Thread 使用时要改掉哪些旧习惯?
1. 不要给 Virtual Thread 建传统线程池
Platform Thread 比较贵,所以过去习惯:
创建固定数量线程
↓
大量任务排队复用线程
Virtual Thread 的思路反过来:
一个并发 Task
↓
一个 Virtual Thread
官方明确建议:
Virtual Thread 不应该被池化复用。
使用:
Executors.newVirtualThreadPerTaskExecutor()
不是在创建一个“固定大小 Virtual Thread Pool”。
它是每个 Task 创建一个新的 Virtual Thread。
2. 不要把 Virtual Thread 当成限流器
以前线程池的数量常常同时承担:
并发限制
Virtual Thread 很便宜以后,这两个概念应该拆开。
例如数据库最多允许 100 个并发查询,可以使用:
Semaphore
连接池容量
业务限流
控制资源并发,而不是为了限流强行把 Virtual Thread 池化。
3. ThreadLocal 要更谨慎
如果 JVM 里只有 200 个池线程:
200 份 ThreadLocal 大对象
可能还能接受。
如果未来出现:
500000 个 Virtual Thread
每个线程都缓存一个昂贵对象,就完全是另一种内存规模。
所以 ThreadLocal 更适合保存真正的线程上下文,而不是把它当作“每线程对象缓存仓库”。
4. synchronized 的 Pinning 规则已经发生过变化
JDK 21 刚交付 Virtual Thread 时,一个重要限制是:
Virtual Thread
↓
进入 synchronized
↓
发生 Blocking Operation
↓
可能 Pin Carrier Thread
这会削弱扩展性。
但从 JDK 24 开始,JEP 491 改进了 Monitor 实现,使 Virtual Thread 在绝大多数 synchronized 场景中也能够在阻塞时 Unmount,不再因为持有 Java Monitor 就长期 Pin Carrier。
所以今天再看到:
Virtual Thread 绝对不能和 synchronized 一起使用。
已经属于版本过时的结论。
当前仍然需要关注 Native / Foreign Function 等可能导致 Pinning 的场景。
九、Process、Platform Thread、Virtual Thread 怎么串起来?
现在把这几个概念一次性放到同一张图上:
其中:
Process
负责提供运行环境和资源边界。
Platform Thread
通常 1:1 对应 OS Thread,由 Linux Scheduler 调度。
Virtual Thread
由 JDK Scheduler 管理,在需要运行时 Mount 到 Platform / Carrier Thread。
最终真正执行机器指令的仍然只有:
CPU Core
十、常见误区
1. 进程和线程
误区 1:程序加载进内存以后才叫程序,运行后才直接“变成”进程。
不够准确。程序是静态代码 / 数据,进程是程序的一次运行实例以及对应的 OS 运行环境,不能只靠“在不在内存”区分。
误区 2:进程有独立内存,线程完全没有自己的内存。
错误。线程共享进程地址空间,但依然有自己的执行栈、寄存器上下文和线程状态。
误区 3:Linux 的线程就是 fork 出来的普通进程。
过度简化。Linux 统一使用 Task 模型,线程通常通过 clone 语义共享地址空间、文件、信号处理并加入同一 Thread Group;POSIX Thread 通常由 pthread / NPTL 封装这些底层机制。
误区 4:Linux PCB 就是一个叫 PCB 的固定结构体。
不是。PCB 是操作系统理论中的通用概念;Linux 真正的核心 Task 描述结构是 struct task_struct。
2. Java Thread
误区 5:每个 Java Thread 一定固定占 1MB。
错误。Platform Thread 的 Stack 和资源消耗受 JVM、OS、架构和配置影响,不是 Java 语言固定常量。
误区 6:操作系统最多只能启动一万个线程。
错误。线程数量没有这种跨机器固定上限,会受到虚拟内存、Stack、PID / Task 限制、容器和内核资源等共同影响。
3. Fiber 与 Virtual Thread
误区 7:Java 现在还要依赖 Quasar 才能使用 Fiber。
过时。Java 21 已正式交付 Virtual Threads。
误区 8:Virtual Thread 就是一个 Platform Thread 里面固定切成很多小线程。
不准确。Virtual Thread 由 JDK Scheduler 调度,可以在生命周期中 Mount 到不同 Carrier Thread,不和某一个 Platform Thread 永久绑定。
误区 9:Virtual Thread 比 Platform Thread 计算速度快。
错误。它提高的是大量阻塞任务的可扩展性和吞吐量,不会让 CPU 执行同一段计算指令突然更快。
误区 10:Virtual Thread 越多,CPU 并行度越高。
错误。真正的 CPU 并行度最终仍受物理 / 逻辑处理器数量约束。
十一、总结
现在可以用三句话区分最核心的几个概念:
Process
= 一次程序运行实例 + OS 资源与隔离边界
Platform Thread
= 通常 1:1 对应 OS Thread 的 Java 执行线程
Virtual Thread
= 由 JDK 调度、按需 Mount 到 Carrier Thread 的轻量 Java Thread
Linux 又进一步把进程和线程统一成:
Task
通过是否共享:
- Address Space;
- File Table;
- Signal State;
- Thread Group;
等资源来形成不同的执行关系。
Java 21 之后,过去很多“Fiber 只是未来方向”的资料已经发生根本变化:
Java Virtual Thread
已经是正式平台能力。
但它解决的问题也必须理解准确:
Virtual Thread 没有增加 CPU 算力,它减少的是“一个业务并发任务长期绑定一个昂贵 OS Thread”的必要性。
所以它真正改变的是高并发服务的编程模型:
过去:
任务很多 → OS Thread 太贵 → 池化 + 异步化
现在:
任务很多 → 每个 Task 一个 Virtual Thread
→ 阻塞时释放 Carrier
→ 保留同步代码的可读性
→ 仍获得很高的并发扩展能力
进程决定资源和隔离边界,OS Thread 决定操作系统如何调度执行,而 Virtual Thread 则让 Java 可以在有限的 OS Thread 之上承载数量巨大的并发任务。
评论区