执行单元的第一性原理(进程 / 线程 / 协程的权衡)与调度模型,见 /操作系统/进程与线程.html;并发的可见性 / 有序性见 /编程语言/JAVA/JVM/JAVA内存模型.html,控制与选型哲学见 /编程语言/并发编程.html。
本篇聚焦 Java 线程的独有落地:线程模型、创建解耦、状态机、虚拟线程。一句话锚点——
Java 线程 = 把"可调度执行流"这一抽象,直接映射为 OS 线程并以对象建模其状态机;虚拟线程则是把映射改写为用户态调度。
Java 选择 Thread 作为一等公民,背后隐含了三条设计哲学:
这决定了:
平台线程(1:1)的特征、瓶颈与演进,见后文「调度模型谱系:M:1 / 1:1 / M:N」。
按稳定性分层,决定哪些知识值得固化、哪些只需备查:
| 层 | 稳定性 | 关注什么 | 详见 |
|---|---|---|---|
| 抽象层 | 不变 | 执行上下文 · 调度 · 阻塞与唤醒 | /操作系统/进程与线程.html |
| JVM 实现层 | 相对稳定 | Thread 对象 · 线程状态机 · wait / park / interrupt | 本篇「线程生命周期与状态机」 |
| 工程使用层 | 易变 | Runnable / Callable · 线程池 · 虚拟线程 | 本篇「调度模型谱系」「虚拟线程」 |
把"做什么"(任务)与"怎么执行"(载体)解耦成正交两维,从而让载体能演进(平台线程→虚拟线程)、能复用(线程池)、能编排(行为对象化)——而任务代码始终不变。
解耦不是"任务 vs 线程"二元,而是三层各司其职——这正是 Command + Executor 思想的完整形态:
| 角色 | 抽象 | 职责 |
|---|---|---|
| 任务(what) | Runnable / Callable | 描述"要做什么" |
| 执行器(policy) | Executor / ExecutorService | 决定"何时、用何种策略调度" |
| 载体(how) | 平台线程 / 虚拟线程 | 实际承载执行的物理单元 |
中间的执行器是关键:它把"调度策略"从"物理载体"中再次剥离,线程池才有地方复用线程、虚拟线程才有 newVirtualThreadPerTaskExecutor 这样的替换点。少了这一层,"谁在复用、谁在演进"便无处落地。
二者都是"做什么",分化点在于能否承载结果与异常:
| 返回值 | 受检异常 | 配对 | |
|---|---|---|---|
| Runnable | 无 | 不能抛 | — |
| Callable | 有 | 可抛 | Future |
Callable 的存在,是为了让被对象化的行为能把结果与异常经 Future 回传——这正是"行为对象化(可编排)"的落点。
线程是一个有始有终、且时序不受你控制的活动。理解它的行为,就是面对两个"不可控":
两者共一个原理——并发的可控性来自约定与协作,而非强制控制。这也是 join / wait / interrupt 能归并为同一套"状态 + 协作信号"机制的根源。
每个状态的"出口钥匙"握在不同角色手里——这张表本质是一张"每个状态我有没有控制权、控制权在谁"的杠杆图:
| 状态 | 进入原因 | 出口钥匙在谁 → 你能做 / 不能做 |
|---|---|---|
| RUNNABLE | 就绪待跑 | 调度器决定何时上核 → 无法控制执行顺序,优先级 / yield 只是建议 |
| BLOCKED | 抢 synchronized 锁失败 | 持锁者释放才出 → 只能改锁设计,不能直接唤醒一个 BLOCKED 线程 |
| WAITING / TIMED_WAITING | 自身 wait / join(本质即在目标线程对象上 wait)/ park | 另一线程发信号(notify / unpark / 中断 / 超时)→ 唤醒靠协作,不能外部强拉 |
| TERMINATED | run 结束或异常 | 只有线程自己能终结 → 外部只能发中断请求,stop() 强杀已废弃 |
用错杠杆即反模式:对 RUNNABLE 用优先级猜调度 → 「编排时序」;对 TERMINATED 期待 stop() 强杀 → 「强制管控」(见后文反模式节)。
中断协议、取消与生命周期管理的完整 Java 落地,见 /编程语言/JAVA/JAVA并发编程/JAVA并发编程.html「取消、中断与生命周期管理」。
线程是执行流,但执行流自己不会获得 CPU——它必须绑定到一个内核可调度实体(OS 线程)才能上核。于是"如何调度线程"只有一个自由度:
M 个执行流 ↔ N 个内核线程的映射比例。
三种经典模型,就是这个比例的三个取值,而非三种孤立技术。
| 模型 | 映射 | 调度者 | 切换成本 | 多核并行 | 一处阻塞的影响 | 代表 |
|---|---|---|---|---|---|---|
| M:1 | 多对一 | 运行库/VM | 极低(用户态) | ✗ 不能 | 钉死全部执行流 | 早期 Java green thread |
| 1:1 | 一对一 | OS 内核 | 高(陷内核) | ✓ 天然 | 仅阻塞自身 | 平台线程(传统 Java) |
| M:N | 多对多 | VM + 内核 | 低(卸载) | ✓(N 核) | 卸载让出 carrier | 虚拟线程 |
M:1 → 1:1:被"多核 + 阻塞"淘汰
早期 OS 线程昂贵甚至缺位,运行时自建调度器(M:1)是务实选择。但它有两个致命伤:
当多核普及、OS 线程成熟,1:1 用"内核统一调度"一次性解决这两点,于是胜出——代价是线程沦为稀缺资源、阻塞即浪费一个 OS 线程(平台线程瓶颈)。
1:1 → M:N:续体让 M:1 的两个致命伤同时消失
M:N 不是回到 M:1,而是螺旋上升——它保留 M:1 的"用户态廉价",又补上 M:1 缺的两件事:
使"卸载"成为可能的前提,是 GC 堆 + 续体让线程栈可挂起/搬迁。这一前提在 green thread 时代并不具备,这正是 M:N "现在才可行"的根因。
由此,用户态执行上下文可正交拆为两件独立的事——这是协程 / 纤程的统一抽象:
| 构件 | 职责 |
|---|---|
| Continuation(续体) | 保存与恢复执行过程(栈、PC),使一段计算可在任意点挂起并稍后续跑 |
| Scheduler(调度器) | 决定续体何时、在哪个 carrier 上恢复运行 |
二者解耦的意义:续体只管"能不能停下再续",调度器只管"何时续"——更换调度策略无需改动业务代码。这也解释了为何 M:N 的核心不是"更快",而是用更低成本管理更大规模的并发。
故虚拟线程并非全新发明,而是 green thread(同为运行时调度的执行上下文)在 M:N + 续体成熟后的重做。
三代演进从未消除权衡,只是不断转移代价:
切换成本 × 多核并行 × 阻塞语义 × 资源稀缺——四者不可同时最优。
它的本质与机制(用户态调度的执行上下文、阻塞即卸载、M:N carrier)已在上节谱系给出——其意义是让**「阻塞语义」与「资源利用率」这对长期冲突首次同时满足**。本节只回答谱系推不出的两件事:它在并发模型全景中的位置,以及何时该用、何时不该用。
| 模型 | 核心思想 | 代价 |
|---|---|---|
| 线程池 | 控制线程数量 | 编程复杂度 |
| Reactor | 非阻塞 IO | 心智负担 |
| 虚拟线程 | 阻塞语义 + 协程调度 | JVM 实现复杂度 |
虚拟线程降低了并发编程的门槛,而非替代所有模型。
三者代价的差别在归属:线程池与 Reactor 把复杂度压给使用者,虚拟线程独家把它压给 JVM 实现——这才是它"降低门槛"的真正机制:代价转移,而非代价消失。
模型不创造资源,只改变"线程"这一资源的分配效率。因此选型只有一个决定性问题:
当前并发的瓶颈,是线程数、CPU 算力,还是下游容量?
模型只能解除"线程数"这一种瓶颈;另外两种,换任何模型都无效。
| 瓶颈 / 负载 | 该用 | 不该用 | 原因 |
|---|---|---|---|
| IO-bound 高并发(>数千) | 虚拟线程 | 池化平台线程(并发被池封顶) | 瓶颈 = 线程稀缺,虚拟线程正好解除 |
| CPU-bound 计算密集 | 平台线程池(≈核数) | 虚拟线程(无收益) | 上限是核数;虚拟线程提供 scale,不提供 speed |
| 下游有硬上限(DB/外部限流) | 信号量 / 连接池显式背压 | "无限线程"的幻觉 | 瓶颈在下游;海量虚拟线程把全量压力打到下游,放大冲击 |
| 低并发(<数千) | 任意,简单优先 | 为虚拟线程徒增认知负担 | 收益不显著 |
虚拟线程解除的是"线程稀缺"这一种封顶。瓶颈不在线程数时,它既不提速,也不该用。
并发能力 = 执行模型 × 调度策略 × 资源成本。
虚拟线程把线程从稀缺资源变为廉价资源,由此打开更高层抽象——结构化并发:任务生命周期跟随代码块,作用域结束自动级联取消与等待。完整 Java 落地见 /编程语言/JAVA/JAVA并发编程/JAVA并发编程.html「取消、中断与生命周期管理」。
线程的反模式几乎都源于同一个根错误——把"执行流"误当成了它不是的东西。五条误区,正是前述五条核心信条各自的反面:
违反信条 1「Thread 是执行载体,不承载业务」——误当业务对象。
违反信条 2「顺序通过同步表达」——误当可精确排序的指令流。
违反信条 3「阻塞是合法的」——误当必须根除的负担。
违反信条 4「中断是协作的」——误当可抢占强杀的傀儡。
违反信条 5「性能来自模型选择」——误当零成本资源。
线程只是"被调度的执行流"。全篇内容,都是围绕这个最小定义的三次展开与一类误用。
五类反模式,无一不是背离了这个最小定义;纠偏方向只有一个——把线程看回它本来的样子:一条被调度的执行流,不多也不少。