技术演进中的开发沉思-347:高效并发(中)
本文深入解析Java线程的实现、调度与状态管理。线程实现方面,HotSpot采用1:1内核线程映射模型,通过轻量级进程(LWP)关联Java线程与操作系统内核线程,支持多核并行但受限于内核线程数量。线程调度采用抢占式策略,线程优先级仅为调度参考,不能保证执行顺序。线程状态分为NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED六种,其中RUNN
今天关注的线程实现与调度是 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 线程数量不能无限制创建—— 过多线程会导致:
- 内核创建大量 KLT,占用大量内存(每个 KLT 约占 1-8MB 栈内存);
- 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。
结论:优先级仅能用于对执行效率有轻微偏好的场景(如核心业务线程设为高优先级),不能作为线程执行顺序的判断依据 —— 若需要保证线程执行顺序,应使用同步工具(如CountDownLatch、synchronized)。
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
最后小结
- 线程实现:HotSpot 采用1:1 内核线程映射模型(Java 线程→LWP→KLT),依赖操作系统内核管理,支持多 CPU 并行,但线程数量受内核限制,需用线程池复用;用户线程(M:1)和混合实现(M:N)因各自缺陷,未被 HotSpot 采用。
- 线程调度:Java 默认使用抢占式调度(内核被动分配 CPU),摒弃协作式调度(避免线程卡死);线程优先级为 1-10 级,默认 5 级,仅为操作系统调度参考,不同系统映射规则不同,绝对不能依赖优先级保证执行顺序。
- 线程状态:JVM 定义六大官方状态(NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED),均在
Thread.State枚举中,与操作系统状态无关;- RUNNABLE 包含就绪和运行中,是 JVM 的合并设计;
- BLOCKED 仅针对 synchronized 锁等待,WAITING/TIMED_WAITING 为主动等待,三者唤醒方式和资源不同;
- 状态流转有明确规则,NEW 状态只能通过
start()进入 RUNNABLE,start()仅能调用一次。
- 核心避坑:① 用
start()启动线程,而非直接调用run();②sleep()不释放锁,wait()释放锁;③ BLOCKED 与 Lock 锁无关;④ 不依赖优先级保证执行顺序;⑤ 线程终止后状态不可逆。
线程实现与调度是 Java 并发的 “基础运行层”,后续的synchronized、volatile、线程池、并发容器等,都是基于这部分内容实现的 —— 掌握这部分,你就能从 “使用线程 API” 升级为 “理解线程底层执行逻辑”,为后续排查并发问题、优化并发程序打下基础。
更多推荐


所有评论(0)