七、线程的底层实现:从 JVM 到操作系统7.1 Java 线程与操作系统线程的映射Java 线程是基于内核线程实现的(1:1 映射模型)。每个 Java 线程对应一个操作系统内核线程,在 Linux 上通过pthread_create创建,由内核调度器负责调度。Windows 上对应 Windows Thread。这是由 JVM 实现的,HotSpot VM 通过 `pthread_create`(Linux)或 `CreateThread`(Windows)来创建真正的 OS 线程。Java 层面: JVM 层面: OS 层面:Thread-1 → JavaThread-1 → pthread-1 (Linux)Thread-2 → JavaThread-2 → pthread-2Thread-3 → JavaThread-3 → pthread-3这种 1:1 的映射模型使得 Java 线程的行为与操作系统线程一致,由 OS 内核负责调度。优点是编程模型简单,缺点是线程创建和上下文切换的开销由 OS 决定,Java 层面无法优化。7.2 线程栈的内存布局每个 Java 线程有自己的栈空间,默认大小由 JVM 参数 `-Xss` 控制(通常 512KB 到 1MB)。高地址┌──────────────────────────┐│ 栈帧 (Frame) │ ← 当前执行方法的栈帧│ ┌────────────────────┐ ││ │ 局部变量表 │ ││ │ 操作数栈 │ ││ │ 动态链接 │ ││ │ 方法返回地址 │ ││ └────────────────────┘ ││ 栈帧 (Frame) │ ← 调用者方法的栈帧│ ┌────────────────────┐ ││ │ ... │ ││ └────────────────────┘ ││ ││ (栈增长方向 ↓) ││ │└──────────────────────────┘低地址如果递归没有正确的终止条件,栈帧不断压入,最终会触发 `StackOverflowError`。这就是为什么无限递归会崩——栈空间用完了。7.3 上下文切换的代价当操作系统把 CPU 从一个线程切换到另一个线程时,需要做以下事情:1. 保存当前线程的寄存器状态、程序计数器到其 TCB(Thread Control Block)2. 更新当前线程的状态(RUNNING → READY)3. 选择下一个要运行的线程(调度算法)4. 从新线程的 TCB 恢复寄存器状态、程序计数器5. 刷新 CPU 缓存(可能,取决于缓存策略)一次上下文切换大约消耗 1-10 微秒(取决于硬件和 OS),看起来不多,但高并发场景下(每秒百万次切换)就非常可观了。更隐蔽的代价是 CPU 缓存失效——新线程的数据大概率不在当前 CPU 核心的 L1/L2 缓存中,需要从主内存加载,延迟从纳秒级跳到几十纳秒。这也是为什么线程数不是越多越好。线程数超过 CPU 核心数后,多出来的线程不会增加并行度,只会增加上下文切换的开销。// 计算上下文切换的实际代价public class ContextSwitchBenchmark { // 两个线程通过 volatile 变量来回传递令牌 private static volatile boolean flag = true; public static void main(String[] args) throws Exception { long start = System.nanoTime(); Thread t1 = new Thread(() - { for (int i = 0; i 1_000_000; i++) { while (!flag) Thread.onSpinWait(); // 忙等 flag = false; } }); Thread t2 = new Thread(() - { for (int i = 0; i 1_000_000; i++) { while (flag) Thread.onSpinWait(); flag = true; } }); t1.start(); t2.start(); t1.join(); t2.join(); long elapsed = System.nanoTime() - start; // 100万次来回 = 200万次上下文切换 System.out.printf("每次切换约 %.0f 纳秒%n", elapsed / 2_000_000.0); // 典型输出:每次切换约 800-1500 纳秒(取决于 OS) }}八、深入理解 happens-before 与 Java 内存模型前面我们说了 `volatile` 保证可见性,`synchronized` 保证互斥和可见性。但"可见性"到底是什么意思?这需要理解 Java 内存模型(JMM)。8.1 重排序:你以为的顺序不是真正的顺序编译器和 CPU 为了优化性能,可能会对指令进行重排序。// 线程 Aint a = 1; // ①int b = 2; // ②int c = a + b; // ③// 线程 BSystem.out.println(c); // 可能看到 0、1、2 或 3?在这个例子中,① 和 ② 之间没有数据依赖,编译器或 CPU 可能会重排序为先执行 ② 再执行 ①。对于单线程程序来说,这不影响结果。但对于多线程程序,另一个线程可能观察到中间状态。更经典的例子是双重检查锁定(DCL)的陷阱:// ❌ 有 Bug 的单例模式public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { if (instance == null) { // 第二次检查 instance = new Singleton(); // 这一行有三个子操作: // ① 分配内存 nbs