在系统底层,线程调度是一个关键但常被忽视的问题。高并发编程中,我们常听说用户态线程无法利用多核,且阻塞会导致整个进程卡死。那么,这背后的原因是什么?Java在不同版本中如何解决这些问题?本文将深入解析。
🧩 一、用户态线程的两个核心缺点
1)无法利用多核 —— 因为内核根本不知道你有“线程”
用户态线程(ULT,User-Level Thread)由用户空间的线程库管理(例如早期 Java 1.1 的 green threads)。
它们在内核中没有任何对应的数据结构。
🔍 内核眼里的世界只有“进程”或“内核线程”系统调度
如果一个进程内部有 100 个用户态线程
但内核只看到 1 个内核线程
那么:
👉 内核调度器只能把这个唯一的内核线程安排到一个 CPU 核上
👉 整个进程无论有多少用户线程,都只能在这一个核心上跑
👉无法利用多核并行能力
这就是典型的1: M 模型:
内核线程 1 ——> 映射 ——> 用户态线程 (M)
所以你看到的“并发”,其实只是用户态库的时间片轮转。
实际表现
- CPU 密集型任务无法并行
- 高并发场景吞吐量上不去
- 机器明明有 8 核,进程却只能用 1 核
2)线程阻塞时,内核无法优化调度
假设某个用户态线程 A 调用了一个阻塞系统调用,例如:
FileInputStream.read(); // 阻塞 I/O
这会导致:
🔸 内核会挂起当前内核执行实体(通常是 1 个)
因为内核根本不知道你有 B、C、D 等其他用户线程可运行。
结果就是:
🚫整个进程都“挂住”了
🚫 其余用户线程无法被调度
🚫 CPU 闲置,资源浪费
为什么用户态库解决不了?
因为:
- 页缺失、文件系统 I/O、锁竞争等阻塞都发生在内核态
- 用户态库根本无法预判
因此当阻塞发生时
内核只能阻塞“它能看到的东西”——内核线程/进程
而不是用户态线程。
🧩 二、Java 的真实实现:从 Green Threads 到 Loom
Java 的线程实现经历了三个时代:
🕰 1)Java 1.1:Green Threads(纯用户态线程)Java
特点:
- 完全由 JVM 在用户空间调度
- JVM 负责线程切换和调度
- I/O 阻塞会导致整个进程阻塞
- 无法利用多核
示意图:
[ JVM 调度器 ]
├── U-Thread 1
├── U-Thread 2
└── U-Thread 3
内核只看到一个 OS Thread
就是上文用户态线程缺点的典型代表。
因此 Java 很快放弃了这种模式。
🚀 2)Java 1.2+ ~ Java 17:Native Threads(1:1 内核线程)
JVM 每创建一个 Thread()
→ 就创建一个原生 OS Thread
这是目前绝大多数服务器 Java 的运行模式。
优势:
劣势:
- OS 线程昂贵
- 上下文切换重
- 创建成本高
- 数量有限(通常几千 ~ 几万)
示意图:
Java Thread 1 ──> OS Thread 1 ──> CPU 核心
Java Thread 2 ──> OS Thread 2 ──> CPU 核心
...
这是典型的1:1 模型。
⚡ 3)Java 19+:Project Loom 虚拟线程(Virtual Threads)
Loom 的虚拟线程(VirtualThread)介于:
- 轻量用户态线程
- 内核协作调度之间,是一种 M:N 模型的新版本。
它解决了用户态线程的两个致命问题:
✔ 能利用多核
因为虚拟线程运行在Carrier Thread(OS Thread)上
由 JVM 的调度器与内核配合。
✔ 阻塞不会阻塞整个进程
因为 Loom 把阻塞 I/O 转换成:
- 非阻塞 I/O
- 协程式挂起(Continuation)
JVM 会在阻塞处自动park虚拟线程
然后把 Carrier Thread 释放给其他虚拟线程。
示意图:
VirtualThread A ──┐
VirtualThread B ──┼──> 运行在 Loom 调度器 ——> 多个 OS Thread
VirtualThread C ──┘
你可以轻松创建几十万甚至百万级线程
且不会造成用户态线程的“全局阻塞”。
🧩 三、用户态线程 vs Java 各代线程实现(对照表)
| 模型 |
Java 时代 |
能否多核 |
阻塞影响 |
可创建线程量 |
本质 |
| 用户态线程 (Green Threads) |
Java 1.1 |
❌ 不行 |
全进程阻塞 |
较多 |
纯用户态 |
| 内核线程 (NPTL) |
Java 1.2+~17 |
✔ 可以 |
仅阻塞该线程 |
少(几千) |
1:1 映射 |
| Java 虚拟线程 (Loom) |
Java 19+ |
✔ 可以 |
不影响其他虚拟线程 |
超多(百万) |
M:N/协程 |
🧩 四、底层流程:用户态线程 vs 内核线程(Java 示例伪代码)
1)用户态线程(Green Thread)模型
用户线程A调用 read() → 内核阻塞 → 整个进程 sleep
用户线程B/C 无法运行
伪代码:
// JVM 用户态调度
for (;;) {
run(nextUserThread());
if (userThreadCallsBlockingIO()) {
// 整个进程会被内核挂起
blockProcess();
}
}
2)Java Native Thread(现代 Thread)
Thread t = new Thread(() -> read()); // 阻塞 read()
t.start();
实际为:
Java Thread → OS Thread → 阻塞只影响当前 TID
3)Loom 虚拟线程
var vt = Thread.startVirtualThread(() -> read());
JVM 内部处理流程:
read() 阻塞点 → JVM 发现 → 使用 Continuation 挂起该虚拟线程
Carrier Thread 被释放 → 去执行其他虚拟线程
🧩 五、总结:为什么要理解这些底层调度?
理解这套机制,你就能看懂:
- 为什么 Golang、Loom 都采用 M:N 模型
- 为什么自己实现协程库必须使用非阻塞 I/O
- 为什么高并发服务器喜欢事件循环(epoll)
- 为什么 OS 线程不能无脑创建几十万个
更重要的是:
你能判断自己系统的并发瓶颈究竟在哪一层——用户态?JVM?内核?I/O?