找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

483

积分

0

好友

64

主题
发表于 2025-11-27 04:13:35 | 查看: 10| 回复: 0

在系统底层,线程调度是一个关键但常被忽视的问题。高并发编程中,我们常听说用户态线程无法利用多核,且阻塞会导致整个进程卡死。那么,这背后的原因是什么?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?




上一篇:Docker日志文件导致磁盘空间爆满的排查与优化
下一篇:Python并发编程实战:多进程、多线程与多协程提速指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区(YunPan.Plus) ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-7 06:47 , Processed in 0.072658 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 CloudStack.

快速回复 返回顶部 返回列表