前言

Java线程的实现是一个经典的“跨层协作”问题。要深入理解它,我们需要将视角分为两层:用户层面的 JVM 和 系统层面的操作系统(OS)

简单来说,JVM 规范只规定了线程应该具备的行为,而具体的实现方式(1:1、N:1 还是 N:M)则由具体的 JVM 实现决定。目前最主流的 HotSpot JVM 在传统的 Java 版本(Java 19 之前)中,采用的是 1:1 模型,即 Java 线程直接映射为操作系统的内核级线程。

操作系统中的线程模型

在理解 JVM 之前,先看 OS 提供了什么。现代操作系统(Linux, Windows, macOS)将线程通常划分为如下两类:

  • 内核线程:内核直接管理的执行单元。
  • 用户线程:应用程序在用户空间模拟的执行流。

本文介绍的线程主要基于Linux系统,因此这里先简单介绍一下Linux中的线程怎么实现:

在 Linux 中可以认为使用进程实现了线程 ,这涉及到 Linux 设计的哲学:不区分进程与线程,一切皆是“任务”。Linux 内核用同一个结构体 struct task_struct 来描述一个可调度的执行实体。无论是我们传统意义上的“进程”,还是“线程”,在内核看来都是一个 task_struct 实例。在Linux系统中Linux线程KLT(Kernel Level Thread)又被称为轻量级进程LWP(Light Weight Process)。

这里面的关键魔法在于 clone(): 创建新的执行实体,无论是进程还是线程,最终都调用 clone() 系统调用。其关键区别在于创建时传递的“资源共享标志位”:

  • 创建传统“进程”:fork() -> 最终调用 clone(),不指定共享标志。结果是:新 task_struct 拥有独立的内存地址空间、文件描述符表、信号处理等。
  • 创建“线程”(LWP):pthread_create() -> 最终调用 clone(),指定共享标志(如 CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND...)。结果是:新 task_struct 共享父任务的内存地址空间、文件系统信息、打开的文件、信号处理程序等。

操作系统中的线程模型,主要围绕内核线程和用户线程之间的关系展开,细节可见下面章节。

1:1 线程模型

对应原理:每个用户线程调用一次 clone,创建一个独立的LWP。它们不共享地址空间映射关系(各自独立看待),完全由内核调度。

  • 由操作系统内核直接管理。

  • 线程的创建、销毁、调度都由 OS 完成(需要切换到内核态)。

  • 每个线程都有独立的内核栈和 PCB(在 Linux 中实际由 task_struct 描述)。

  • 优点:可以利用多核 CPU,一个线程阻塞了,其他线程还可以运行。

  • 缺点:上下文切换开销大(User Space <-> Kernel Space),线程创建较重。

N:1 线程模型

对应原理:只调用了一次 clone 创建一个 LWP。所有的用户线程都在用户空间模拟,内核只“看”到这唯一的一个 LWP。

  • 优点:创建、销毁、切换极快,无需内核介入。
  • 缺点:无法利用多核资源(内核只分配一个 CPU 给该进程),如果一个线程阻塞(如发起 IO),整个进程都会阻塞。

N:M 线程模型

对应原理:调用了 N 次 clone 创建 N 个 LWP 组成 LWP 组。M 个用户线程动态绑定到这些 LWP 上。结合了 LWP 的并行能力和用户态线程的轻量。

  • 优点:结合了前两个模型的优点
  • 缺点:实现复杂,目前GO语言的GMP 模型和Java的Loom都是 M:N 的教科书级实现。

JVM 线程模型

1. 一对一模型 (1:1 Model)

现状: HotSpot JVM (Java 1.2 - Java 18 默认模式)

图解特点:

  • 直接绑定:每一个 Java 线程对象都直接拥有一个专属的 OS 内核线程。
  • 调度权:箭头指向 OS,表示由操作系统直接调度到 CPU 上。
classDiagram
    %% 定义 JVM 用户态层
    class JavaThread {
        +run()
        +start()
    }

    %% 定义 OS 内核态层
    class OSThread {
        +task_struct
        +Kernel Stack
    }

    %% 定义 CPU 硬件层
    class CPU {
        +Core 1
        +Core 2
    }

    %% 映射关系 (1:1)
    JavaThread "1" --> "1" OSThread : 直接映射

    %% 调度关系
    OSThread --> CPU : 由OS调度器分配
    
    note for JavaThread "JVM 层面 (用户态)"
    note for OSThread "OS 层面 (内核态)<br>如:Linux LWP"

文字解析:

  1. JavaThread 在堆中被实例化。
  2. 调用 start() 时,JVM 调用底层 API(如 pthread_create)创建一个 OSThread
  3. JavaThread 的所有操作都由这个唯一的 OSThread 执行。如果 JavaThread 阻塞,OSThread 也会阻塞,被 OS 挂起。

2. 多对一模型 (N:1 Model)

历史: JVM 早期“绿色线程”

图解特点:

  • 多路复用:多个 Java 线程对象复用同一个 OS 内核线程。
  • 调度权:箭头指向 JVM,表示由 JVM 内部的调度器决定谁在运行,OS 根本不知道上面有 N 个线程。
classDiagram
    class JVM_Scheduler {
        +用户态调度器
        +run_queue
    }

    class JavaThread {
        +run()
    }

    class OSThread {
        +task_struct
    }

    class CPU {
        +Core 1
    }

    %% JVM 中的多个线程
    JavaThread "N" ..> "1" JVM_Scheduler : 注册到调度器
    
    %% JVM 调度器绑定唯一 OS 线程
    JVM_Scheduler --> "1" OSThread : 绑定

    %% OS 调度
    OSThread --> CPU

    note for JVM_Scheduler "JVM 自己管理切换<br>(无需进入内核态)"
    note for JavaThread "用户级线程<br>(绿色线程)"

文字解析:

  1. JVM 内部维护了一个调度器 (JVM_Scheduler)。
  2. JavaThread A, B, C 都在用户空间,不涉及内核创建。
  3. 它们共享那唯一的 OSThread
  4. 致命缺陷:如果 JavaThread A 发起了 I/O 阻塞,底层的 OSThread 就会卡住,导致 JavaThread B 和 C 即使就绪也无法运行(因为 OS 挂起了这唯一的线程)。

3. 多对多模型 (M:N Model)

未来: Java 21+ 虚拟线程 / Go Goroutine

图解特点:

  • 两级调度:M 个 Java 线程映射到 N 个 OS 线程(通常 M >> N)。
  • 解耦:Java 线程的阻塞不再直接等同于 OS 线程的阻塞,JVM 可以动态调整映射关系。
classDiagram
    class JVM_Scheduler {
        +用户态调度器
        +任务队列
    }

    class VirtualThread {
        +run()
    }

    class CarrierThread {
        +OS Thread (LWP)
    }

    class CPU {
        +Multi-Core
    }

    %% M 个虚拟线程注册到 JVM 调度器
    VirtualThread "M" ..> "1" JVM_Scheduler : 调度

    %% N 个载体线程 (OS 线程)
    JVM_Scheduler "1" --> "N" CarrierThread : 动态映射
    
    %% OS 调度载体线程
    CarrierThread --> CPU : 并行执行

    note for VirtualThread "轻量级用户线程(Virtual Thread)"
    note for CarrierThread "内核线程<br>数量固定"
    note for JVM_Scheduler "M:N 关键<br>处理阻塞与挂载"

文字解析:

  1. VirtualThread(虚拟线程)非常轻量,可以创建成千上万个。
  2. JVM 内部的 JVM_Scheduler 负责将虚拟线程“Mount”(挂载)到 CarrierThread(载体线程/OS线程)上执行。
  3. 当一个 VirtualThread 遇到阻塞(如读网络)时,JVM 会将其卸载,CarrierThread 立刻空闲下来去执行其他的 VirtualThread
  4. 充分利用了多核资源(因为 N > 1),同时保持了用户态线程的高并发能力。

总结

最后再给出一个Java运行线程的示例分析:

Thread t = new Thread(() -> {
    // 这段代码会在一个真正的内核线程上运行
    System.out.println(Thread.currentThread().getName());
});
t.start();  // 这里发生了从Java到OS的“协议转换”

启动一个Java线程涉及:

  1. Java层:分配Thread对象、初始化栈大小、设置优先级
  2. JVM层:分配Java栈、创建Java线程结构、注册到GC
  3. 本地层:调用pthread_create、绑定到JVM内部数据结构
  4. OS层:创建task_struct、分配内核栈、加入调度队列
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com