深度剖析:在 Java 里 new Thread(),底层到底发生了什么?
深度剖析:在 Java 里new Thread(),底层到底发生了什么?
在日常的业务开发中,我们对new Thread().start()这行代码再熟悉不过了。但你是否曾停下来思考过:我们在 Java 中创建的这个线程,和操作系统底层的物理线程到底是什么关系?是一对一的,还是多对一的?
这个问题,往往是面试中区分“初级 CRUD 工程师”和“高级架构师”的一道经典分水岭。
今天,我们就来直接亮出底牌:在大家目前最常用的主流 Java 版本(如 JDK 8 到 JDK 17)中,你创建的 Java 线程与操作系统的底层物理线程是绝对的**一对一(1:1)**关系!
但这只是冰山一角。要想真正看透 Java 并发编程的底层逻辑,我们必须翻开 Java 线程模型演进的“前世、今生和未来”。这是一部非常精彩的进化史。
阶段一:前世(Java 1.1 时代)—— 多对一(M:1)的“绿色线程”
在极其古老的 Java 版本中,操作系统根本不知道 Java 有多线程这回事。
- 怎么做的:JVM 在用户态(内存里)自己模拟了一大堆线程,这些被称为绿色线程(Green Threads)。成百上千个 Java 线程实际上只对应操作系统里的 1 个物理线程。
- 致命缺陷:因为操作系统只给 JVM 分配了 1 个物理线程的调度“坑位”,一旦某个 Java 线程发生了 I/O 阻塞(比如读写文件、等待网络响应),操作系统会直接把这唯一的一个物理线程挂起。结果就是,整个 JVM 进程里的所有 Java 线程全军覆没,跟着一起卡死。更糟糕的是,这种模式根本无法利用多核 CPU 的并行计算性能。
结局:由于天然的性能缺陷,绿色线程很快就被官方彻底抛弃了。
阶段二:今生(Java 1.2 ~ Java 20)—— 一对一(1:1)的“内核线程”
痛定思痛后,Java 迎来了漫长的 1:1 时代,这也是我们现在每天都在用的模式。
- 怎么做的:当你用 Java 代码写下
new Thread().start()时,JVM 底层会直接调用操作系统的 API(例如 Linux 下的clone()系统调用)。操作系统会在内核里为你硬生生地创建一个真实的物理线程(Kernel Thread)。 - 谁来调度:此时 JVM 彻底当了甩手掌柜。Java 线程什么时候执行、被分配到哪个 CPU 核心上、什么时候被切走,全部交由底层的操作系统内核去全权调度。
优势与痛点并存:
- 优势:完美利用了多核 CPU 的性能。一个 Java 线程阻塞了,操作系统会自动把 CPU 切给另一个线程,互不影响。
- 痛点(极度沉重):
- 太占内存:在 Linux 下,创建一个物理线程默认要分配 1MB 的栈内存。如果你敢在 Java 里同时创建 10 万个线程,内存瞬间就会被打爆,直接引发 OOM。
- 切换太慢:操作系统在物理线程之间进行上下文切换(Context Switch)时,需要频繁在“用户态”和“内核态”之间穿梭,保存和恢复各种硬件寄存器,性能损耗极其严重。
阶段三:未来(Java 21 及以后)—— 多对多(M:N)的“虚拟线程”
1:1 模型虽然稳定,但实在太重了,根本应对不了现在动辄百万并发的互联网高吞吐场景(比如 Go 语言的协程 Goroutine 就凭借轻量级大杀四方)。于是,Java 官方憋出了一个大招:Project Loom(虚拟线程 Virtual Threads)。
- 怎么做的:Java 在 JDK 21 中正式引入了虚拟线程,将线程模型进化为 M 个虚拟线程映射到 N 个物理线程上(M:N)。
- 极其恐怖的性能:虚拟线程完全由 JVM 在用户态管理,不再需要向操作系统申请物理线程。它的体积只有区区几百字节!你现在可以在一台普通的笔记本电脑上,轻松写个 for 循环同时启动 100 万个虚拟线程,内存连一丁点波澜都不会有。
- 底层原理:当一个虚拟线程遇到网络阻塞等 I/O 操作时,JVM 会迅速把它从底层的物理线程(载体线程)上“拔”下来,把宝贵的 CPU 坑位让给其他需要计算的虚拟线程,实现了真正的“零阻塞浪费”。
总结:一个通俗的终极比喻
为了更直观地理解,我们可以打个比方:
1:1 内核线程(现在的 Java):就像每个乘客(Java 线程)都配了一辆专属的**出租车(物理线程)**和司机。好处是随时待命,坏处是马路(CPU)很容易堵死,而且买车成本极高。
M:N 虚拟线程(未来的 Java):就像是高铁。成千上万的乘客(虚拟线程)只需要很少的几节车厢(物理线程)就能高效运走,上下车(上下文切换)非常快。
正因为我们目前广泛使用的 Java 1:1 物理线程是极其昂贵的系统资源(创建和销毁极其耗时),所以我们在平时的业务代码中,绝对不能频繁地去new Thread(),而是必须使用“线程池(ThreadPool)”把它们圈养起来重复利用!
