今天关注的线程实现与调度是 Java 并发的基础运行层核心,承接 JMM 的抽象规则,落地为实际的线程执行逻辑 —— 它回答了 “Java 线程在底层如何实现”“CPU 如何分配给线程执行”“线程在生命周期中有哪些状态及如何转换” 三个核心问题。其中 HotSpot 虚拟机采用内核线程映射的 1:1 实现方式,Java 默认使用抢占式调度分配 CPU 资源,线程生命周期遵循新建→运行→阻塞→等待→超时等待→终止的六大状态流转,这三部分内容层层递进,是理解后续线程同步、线程池的基础。

本次解析会紧扣你提到的核心要点,同时补充实战坑点、状态转换细节、底层实现原理,让你不仅知其然,更知其所以然 —— 比如为什么 Java 线程优先级不能靠它保证执行顺序、为什么 RUNNABLE 状态包含 “就绪” 和 “运行中”、为什么 BLOCKED 和 WAITING 要区分开。

一、核心定位

在 Java 并发体系中,线程是执行并发代码的最小单位,而 “实现方式” 决定了 Java 线程与操作系统底层线程的关联关系,“调度方式” 决定了 CPU 资源如何分配给各个线程,“线程状态” 描述了线程从创建到销毁的完整生命周期 —— 这三部分是Java 线程从 “抽象对象” 到 “实际执行体” 的关键,也是解决线程阻塞、死锁、调度不均等问题的基础。

需要先明确一个关键前提:Java 线程的实现和调度高度依赖底层操作系统,JVM 只是做了一层封装,屏蔽了不同系统的底层差异,让 Java 线程能跨平台运行(比如 Windows 和 Linux 的线程实现不同,但 Java 线程的使用方式一致)。

二、线程实现方式

线程的实现方式本质是 **“用户态线程” 与 “内核态线程” 的关联方式 **,主流分为 ** 内核线程映射(1:1)、用户线程(M:1)、混合实现(M:N)** 三种模型。HotSpot 虚拟机(JDK1.5+)默认采用内核线程映射模型,这是 Java 线程的核心实现方式,另外两种作为对比理解即可。

先明确两个基础概念,避免后续混淆:

  • 内核态线程(KLT):由操作系统内核直接管理的线程,内核负责线程的创建、调度、销毁、上下文切换,CPU 资源分配的最小单位是内核线程;
  • 用户态线程(ULT):由 ** 用户程序(如 JVM)** 管理的线程,完全运行在用户态,内核无法感知其存在,无需内核参与上下文切换。

1. 内核线程映射(1:1 模型)——HotSpot 默认实现

核心原理

又称 ** 轻量级进程(LWP)** 实现,一个 Java 线程对应一个轻量级进程(LWP),一个 LWP 又映射一个操作系统内核线程(KLT),形成 1:1 的关联关系。

其中轻量级进程(LWP)是内核线程的用户态表示,是 Java 线程与内核线程之间的 “桥梁”——JVM 操作 Java 线程,实际是通过 LWP 调用内核的线程接口,由内核管理底层的 KLT 和 CPU 资源分配。

核心特性(HotSpot 选择它的原因)
  • 优点:① 内核直接管理线程,支持多 CPU 并行执行(一个线程对应一个内核线程,可跑在不同 CPU 上);② 线程的阻塞(如 IO 阻塞、锁阻塞)由内核处理,不会导致整个进程阻塞;③ 实现简单,JVM 只需封装内核线程接口,无需自己管理线程调度;
  • 缺点:① 线程的创建、销毁、上下文切换需要内核参与,有一定的性能开销;② 线程数量受内核线程数量限制(内核能创建的 KLT 有限,过多 Java 线程会导致频繁上下文切换)。
底层细节

HotSpot 在不同操作系统上的 LWP 实现不同:Linux 下基于pthread(POSIX 线程)实现,Windows 下基于Win32 Thread实现,JVM 通过 JNI 调用底层系统接口,让 Java 线程的创建、启动、终止对应底层 LWP/KLT 的操作。

2. 用户线程(M:1 模型)—— 未被 HotSpot 采用

核心原理

多个用户态线程(Java 线程)对应一个内核态线程(KLT),形成 M:1 的关联关系。所有线程的创建、调度、销毁、上下文切换都由JVM 自己管理,内核完全无法感知用户线程的存在,仅对进程进行管理。

核心特性
  • 优点:① 线程操作(创建、切换、销毁)完全在用户态执行,开销极小,支持创建大量线程;② 无需内核参与,调度策略可由 JVM 自定义,适配 Java 场景;
  • 缺点:① 不支持多 CPU 并行(所有用户线程都映射到一个内核线程,只能跑在一个 CPU 上);② 若一个用户线程发生阻塞(如 IO 阻塞),内核会认为整个进程阻塞,导致所有用户线程都无法执行;③ JVM 需要自己实现复杂的线程调度器,开发维护成本高。
未被采用的原因

无法利用多 CPU 的并行能力,且阻塞问题无法解决,完全不适应现代多核服务器的场景,因此 HotSpot 从未采用该模型。

3. 混合实现(M:N 模型)—— 折中方案,极少使用

核心原理

结合 1:1 和 M:1 的优点,多个用户态线程(Java 线程)映射到多个内核态线程(KLT),形成 M:N 的关联关系。其中:

  • 上层:JVM 管理用户线程,负责用户线程的创建、轻量级调度、上下文切换;
  • 下层:内核管理内核线程,负责 CPU 资源分配、硬件阻塞处理;
  • 中间:JVM 将用户线程动态映射到内核线程,实现多 CPU 并行,且单个用户线程阻塞不会导致所有线程阻塞。
核心特性
  • 优点:兼顾用户线程的低开销和内核线程的并行能力,是理论上的最优解;
  • 缺点:① JVM 和内核的双重调度会导致调度逻辑极其复杂;② 不同操作系统的内核线程接口差异大,跨平台实现难度高;③ 调度策略的优化需要深入结合硬件特性,开发维护成本极高。
应用场景

仅在少数小众虚拟机 / 语言中使用(如 Go 语言的 Goroutine、Erlang 的进程),HotSpot 因实现复杂度和跨平台需求,未采用该模型。

4. 三种实现模型核心对比表

实现模型 关联关系 管理主体 多 CPU 并行 阻塞影响 性能开销 HotSpot 是否采用
内核线程映射(1:1) Java 线程:LWP:KLT=1:1:1 操作系统内核 ✅ 支持 单个线程阻塞,不影响其他线程 中(内核参与操作) ✅ 默认采用
用户线程(M:1) Java 线程:KLT=M:1 JVM 自身 ❌ 不支持 单个线程阻塞,整个进程阻塞 低(用户态操作) ❌ 未采用
混合实现(M:N) Java 线程:KLT=M:N JVM + 内核 ✅ 支持 单个线程阻塞,不影响其他线程 低 - 中(双重调度) ❌ 未采用

5. 1:1 模型的线程数量限制

因 HotSpot 采用 1:1 内核线程映射,Java 线程数量不能无限制创建—— 过多线程会导致:

  1. 内核创建大量 KLT,占用大量内存(每个 KLT 约占 1-8MB 栈内存);
  2. CPU 频繁进行线程上下文切换(保存 / 恢复线程状态),导致 CPU 利用率大幅下降,程序运行变慢。

解决方案:使用线程池管理线程(如ThreadPoolExecutor),限制核心线程数和最大线程数,复用线程,避免频繁创建 / 销毁线程。

三、线程调度

线程调度的核心是 **“操作系统如何将 CPU 的时间片分配给各个线程”,决定了线程的执行顺序。Java 的线程调度完全依赖底层操作系统,JVM 仅做简单的封装,核心规则是抢占式调度 **,线程优先级仅作为操作系统调度的 “参考依据”,而非强制规则。

1. 两种主流调度方式

操作系统的线程调度方式分为两种,Java仅支持抢占式调度,摒弃了协作式调度,这是保证并发程序健壮性的关键。

(1)抢占式调度(Java 默认)

核心规则:由操作系统内核决定 CPU 时间片的分配和回收,线程会被 “被动” 让出 CPU—— 即使线程的任务还没执行完,内核也能在时间片用完后,将 CPU 切换给其他线程;高优先级的线程可以抢占低优先级线程的 CPU 时间片。

核心优点:① 线程执行不会 “独占” CPU,避免单个线程卡死导致整个程序无响应;② 多线程执行更公平,CPU 资源分配可控;③ 适应多核场景,能充分利用 CPU 资源。

Java 中的体现:我们编写的 Java 多线程代码,无需手动让渡 CPU,内核会自动完成线程的切换,比如两个线程同时执行循环,内核会交替分配 CPU 时间片,让两个线程都能执行。

(2)协作式调度(Java 未采用)

核心规则:由线程自身决定何时让出 CPU,** 线程 “主动” 调用让步方法(如 yield)** 后,CPU 才会切换给其他线程;若一个线程一直不主动让步,会独占 CPU,导致其他线程无法执行。

核心缺点:① 若一个线程因 bug 卡死(未主动让步),会导致整个程序无响应;② 调度不公平,执行速度快的线程会占用更多 CPU 资源;③ 不适应现代多核场景。

未被采用的原因:健壮性差,无法处理线程卡死的情况,不符合 Java“跨平台、高可用” 的设计目标。

2. Java 线程优先级

Java 为线程提供了优先级机制,理论上高优先级的线程会获得更多的 CPU 时间片,但因 Java 线程依赖操作系统内核调度,优先级只是对操作系统的 “建议”,而非强制规则—— 操作系统会根据自身的调度策略,将 Java 线程的优先级映射为底层内核线程的优先级,映射过程可能存在 “精度丢失” 或 “忽略”。

(1)Java 优先级的核心定义

Java 通过java.lang.Thread类的常量和方法定义优先级,共分为1-10 级,默认优先级为 5:

  • 最低优先级:Thread.MIN_PRIORITY = 1
  • 默认优先级:Thread.NORM_PRIORITY = 5(主线程的优先级为 5,子线程默认继承父线程的优先级);
  • 最高优先级:Thread.MAX_PRIORITY = 10
  • 操作方法:setPriority(int newPriority)设置优先级,getPriority()获取优先级。
(2)基于操作系统映射,非跨平台一致

不同操作系统的内核线程优先级体系不同,Java 的 1-10 级优先级会被映射为底层系统的优先级,而非直接对应,导致优先级的效果在不同系统上不一致:

  • Linux:内核线程优先级分为 0-99 级(实时优先级),Java 的 1-10 级会简单映射为 Linux 的某个区间(如 1 对应 10,10 对应 90),效果相对明显;
  • Windows:内核线程优先级分为 0-31 级,Java 的 1-10 级仅映射为 Windows 的 6 个优先级级别,存在精度丢失(如 Java 的 3-5 级都映射为 Windows 的同一级别);
  • 部分系统:会忽略 Java 的优先级设置,所有线程使用相同的默认优先级。
(3)绝对不要依赖优先级保证执行顺序

这是 Java 并发的高频坑点 ——优先级只是操作系统的调度参考,不能保证高优先级线程一定先执行,也不能保证高优先级线程获得更多的 CPU 时间片

反例:高优先级线程可能因操作系统的调度策略,迟迟得不到 CPU 时间片,低优先级线程反而先执行:

// 线程A:高优先级
Thread t1 = new Thread(() -> {while (true) {}}, "t1");
t1.setPriority(Thread.MAX_PRIORITY);

// 线程B:低优先级
Thread t2 = new Thread(() -> {System.out.println("低优先级线程先执行");}, "t2");
t2.setPriority(Thread.MIN_PRIORITY);

t1.start();
t2.start();

结果:线程 t2(低优先级)大概率会先执行,因为线程 t1 的死循环会被内核做调度优化,暂时让出 CPU。

结论:优先级仅能用于对执行效率有轻微偏好的场景(如核心业务线程设为高优先级),不能作为线程执行顺序的判断依据 —— 若需要保证线程执行顺序,应使用同步工具(如CountDownLatchsynchronized)。

3. Java 的调度辅助方法

JVM 提供了两个轻量级的调度辅助方法,用于提示操作系统进行线程调度,注意是 “提示” 而非 “强制”,操作系统可以忽略:

  • Thread.yield():线程主动让出当前的 CPU 时间片,回到就绪状态,与其他线程重新竞争 CPU 时间片 —— 仅对同优先级的线程有效,高优先级线程调用 yield () 后,仍会优先竞争到 CPU;
  • Thread.join():让当前线程等待目标线程执行完成后,再继续执行 —— 本质是线程的等待,而非调度,通过wait()实现,会释放锁(后续状态部分详细讲)。

四、线程状态

Java 线程的生命周期分为新建、运行、阻塞、等待、超时等待、终止六大状态,这是JVM 层面的官方状态,定义在java.lang.Thread.State枚举中,与操作系统的线程状态无关 —— 这六大状态覆盖了线程从创建到销毁的所有阶段,明确的状态流转规则是排查线程阻塞、死锁的关键。

1. RUNNABLE 状态包含 “就绪” 和 “运行中”

这是最容易混淆的点 ——JVM 将操作系统的 “就绪态” 和 “运行中态” 合并为一个 RUNNABLE 状态,原因是:

  • 就绪态:线程已创建,拿到所有资源,等待操作系统分配 CPU 时间片;
  • 运行中态:线程获得 CPU 时间片,正在执行run()方法;
  • JVM 层面:无法感知操作系统的 CPU 分配细节,因此将这两个状态合并,统称为 RUNNABLE(可运行态)。

简单说:只要线程没有处于阻塞、等待、超时等待、终止状态,就属于 RUNNABLE 状态,无论是否获得 CPU 时间片。

2. 六大状态核心解析

每个状态都有明确的进入条件退出条件,结合代码示例和实际场景解析,让你直观理解。

状态名称 枚举值 核心含义 进入条件(触发场景) 退出条件(转换方向)
新建 NEW 线程对象已创建,但未调用start()方法,未与底层 LWP/KLT 关联 Thread t = new Thread();(仅创建对象) 调用t.start() → 进入 RUNNABLE
运行 RUNNABLE 线程可运行(包含就绪 + 运行中),已与底层 LWP/KLT 关联,等待 / 正在占用 CPU 调用t.start();从其他状态(阻塞 / 等待)恢复 ① 时间片用完 → 仍为 RUNNABLE;② 触发阻塞 / 等待 / 超时等待 → 对应状态;③ run()执行完 → TERMINATED
阻塞 BLOCKED 线程因竞争 synchronized 同步锁失败而等待,被动阻塞 抢 synchronized 锁 / 内置锁失败 抢到 synchronized 锁 → 回到 RUNNABLE
等待 WAITING 线程无超时等待,需被其他线程主动唤醒,否则永久等待 调用Object.wait()(无参)、Thread.join()(无参)、LockSupport.park() 其他线程调用Object.notify()/notifyAll()LockSupport.unpark()、目标线程执行完(join)→ 回到 RUNNABLE
超时等待 TIMED_WAITING 线程有超时等待,超时后自动唤醒,也可被其他线程提前唤醒 调用Thread.sleep(long)Object.wait(long)Thread.join(long)LockSupport.parkNanos()/parkUntil() ① 超时自动唤醒;② 其他线程提前唤醒 → 回到 RUNNABLE
终止 TERMINATED 线程生命周期结束,run()方法执行完成或因未捕获异常终止 run()执行完毕;② 线程抛出未捕获的 Exception/Error 无(状态不可逆,终止后无法恢复)

3. 核心状态区分

这三个状态都属于 “非运行态”,新手极易混淆,核心区别在于 “等待的原因” 和 “唤醒方式”,这是排查线程问题的关键:

(1)BLOCKED
  • 等待的资源:synchronized 内置锁
  • 唤醒方式:自动唤醒(抢到锁时);
  • 关键特征:等待过程中不会释放已持有的锁(因为还没抢到锁)。
(2)WAITING
  • 等待的资源:其他线程的唤醒信号
  • 唤醒方式:被动唤醒(其他线程调用 notify/unpark);
  • 关键特征:调用wait()/join()前必须持有锁,调用后会释放已持有的锁(让其他线程能抢到锁)。
(3)TIMED_WAITING
  • 等待的资源:CPU 时间片(sleep)其他线程的唤醒信号(wait/join)
  • 唤醒方式:自动唤醒(超时)被动唤醒(提前)
  • 关键特征:① sleep(long)不会释放任何锁(这是与wait(long)的核心区别);② wait(long)/join(long)会释放已持有的锁。

4. 六大状态完整流转图(核心)

用直观的流程图展示线程状态的合法转换关系(部分转换不合法,如 NEW 不能直接到 TERMINATED,BLOCKED 不能直接到 WAITING):

flowchart LR
    A[新建NEW] -->|start()| B[运行RUNNABLE]
    B -->|抢synchronized锁失败| C[阻塞BLOCKED]
    C -->|抢到synchronized锁| B
    B -->|调用无参wait/join/park| D[等待WAITING]
    D -->|notify/notifyAll/unpark/join完成| B
    B -->|调用sleep/带参wait/带参join| E[超时等待TIMED_WAITING]
    E -->|超时/提前唤醒| B
    B -->|run()执行完/未捕获异常| F[终止TERMINATED]
    C -->F
    D -->F
    E -->F

关键注意点:阻塞(C)、等待(D)、超时等待(E)状态都可以直接转换为终止(F)—— 若线程在等待时,被其他线程中断(interrupt())且未捕获中断异常,会直接终止。

5. 实战高频坑点:状态相关的常见错误

(1)调用run()方法而非start(),线程始终处于 NEW 状态

Thread t = new Thread(() -> {System.out.println("执行");});
t.run(); // 错误:直接调用run(),仅为普通方法调用,未创建底层线程
System.out.println(t.getState()); // 输出:NEW

正确做法:调用t.start(),JVM 会创建底层 LWP/KLT,线程进入 RUNNABLE 状态,由内核调度执行run()方法。

(2)混淆sleep()wait(),误以为 sleep 会释放锁
Object lock = new Object();
// 线程A
new Thread(() -> {
    synchronized (lock) {
        try {
            Thread.sleep(10000); // 睡眠10秒,不释放lock锁
        } catch (InterruptedException e) {}
    }
}).start();

// 线程B
new Thread(() -> {
    synchronized (lock) { // 会阻塞10秒,因为线程A睡眠时未释放锁
        System.out.println("抢到锁");
    }
}).start();

核心区别sleep(long)线程调度方法,仅让线程让出 CPU,不会释放任何锁wait(long)同步方法,调用后会释放持有的锁,让其他线程能抢到锁。

(3)认为 BLOCKED 状态包含 Lock 锁的等待

很多新手认为使用java.util.concurrent.locks.Lock(如 ReentrantLock)抢锁失败时,线程会进入 BLOCKED 状态,实际错误

  • synchronized锁失败 → BLOCKED 状态;
  • Lock锁失败 → WAITING/TIMED_WAITING 状态(因为 Lock 底层通过LockSupport.park()实现等待)。

结论:BLOCKED 状态仅针对 synchronized 内置锁,是 JVM 为了区分 “内置锁等待” 和 “其他等待” 设计的。

(4)多次调用start()方法,抛出 IllegalThreadStateException

线程的start()方法只能调用一次—— 第一次调用后,线程进入 RUNNABLE 状态,与底层 LWP/KLT 关联;再次调用时,JVM 会检测到线程已非 NEW 状态,直接抛出异常:

Thread t = new Thread();
t.start();
t.start(); // 抛出:IllegalThreadStateException

最后小结

  1. 线程实现:HotSpot 采用1:1 内核线程映射模型(Java 线程→LWP→KLT),依赖操作系统内核管理,支持多 CPU 并行,但线程数量受内核限制,需用线程池复用;用户线程(M:1)和混合实现(M:N)因各自缺陷,未被 HotSpot 采用。
  2. 线程调度:Java 默认使用抢占式调度(内核被动分配 CPU),摒弃协作式调度(避免线程卡死);线程优先级为 1-10 级,默认 5 级,仅为操作系统调度参考,不同系统映射规则不同,绝对不能依赖优先级保证执行顺序。
  3. 线程状态:JVM 定义六大官方状态(NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED),均在Thread.State枚举中,与操作系统状态无关;
    • RUNNABLE 包含就绪和运行中,是 JVM 的合并设计;
    • BLOCKED 仅针对 synchronized 锁等待,WAITING/TIMED_WAITING 为主动等待,三者唤醒方式和资源不同;
    • 状态流转有明确规则,NEW 状态只能通过start()进入 RUNNABLE,start()仅能调用一次。
  4. 核心避坑:① 用start()启动线程,而非直接调用run();② sleep()不释放锁,wait()释放锁;③ BLOCKED 与 Lock 锁无关;④ 不依赖优先级保证执行顺序;⑤ 线程终止后状态不可逆。

线程实现与调度是 Java 并发的 “基础运行层”,后续的synchronizedvolatile、线程池、并发容器等,都是基于这部分内容实现的 —— 掌握这部分,你就能从 “使用线程 API” 升级为 “理解线程底层执行逻辑”,为后续排查并发问题、优化并发程序打下基础。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐