线程和多线程模型
引入进程解决了”多个活动并发推进”的问题,但留下一个新麻烦:进程太重了。这一节讲的就是怎么把它变轻,以及变轻之后带来的一整套新边界。
机制
进程为什么重
进程既是资源分配的单位,又是调度的单位。这两个身份绑在一起,导致每次切换进程都要付出资源层面的代价:
切换进程时,除了保存寄存器现场,还必须切换地址空间——换页表基址寄存器,随之而来的是 TLB 全部失效,Cache 命中率也会掉。之后新进程每访问一个页都要重新走一遍地址转换、重新填 TLB。这部分开销远大于保存那几十个寄存器。
而现实中大量场景其实不需要换地址空间。一个浏览器要同时渲染页面、下载资源、响应点击,这三件事共享同一份数据,本来就该在同一个地址空间里。为它们各开一个进程,等于每次切换都白白付一次页表和 TLB 的代价。
拆分:资源归进程,调度归线程
解决办法是把进程的两个身份拆开:
进程仍是资源分配的基本单位,线程成为调度的基本单位。
这是引入线程后最核心的一句话,几乎所有考点都从它推出来。
于是同一进程内的多个线程:
- 共享:地址空间(代码段、数据段、堆)、打开的文件、信号处理方式等资源
- 私有:程序计数器、一组寄存器、栈,以及线程控制块 TCB
私有的这三样正是”执行流”的最小必要状态——每条执行流得知道自己跑到哪(PC)、算到一半的中间值在哪(寄存器)、函数调用链是什么(栈)。除此之外的东西都可以共享。
因为地址空间不变,同进程内的线程切换不需要换页表,TLB 不失效,开销比进程切换小一个量级。这就是线程”轻”的全部来源。
用户级线程与内核级线程
线程可以在两个层次上实现,判据只有一个:内核知不知道它的存在。
flowchart TB subgraph ULT["用户级线程 ULT 内核不知情"] direction TB U1(线程1) & U2(线程2) & U3(线程3) --> LIB["线程库<br/>(用户态)"]:::user LIB --> UP["内核眼中:<br/><b>一个</b>进程"]:::kern end subgraph KLT["内核级线程 KLT 内核直接调度"] direction TB K1(线程1) --> KA["内核<br/>TCB1"]:::kern K2(线程2) --> KB["内核<br/>TCB2"]:::kern K3(线程3) --> KC["内核<br/>TCB3"]:::kern end classDef user fill:#dbeafe,stroke:#2563eb,color:#1e3a5f classDef kern fill:#fef3c7,stroke:#d97706,color:#78350f
用户级线程(ULT):完全由用户态的线程库管理,TCB 也在用户空间。内核只看到一个进程,对里面有几条线程一无所知。
- 优点:切换不需要进内核,就是用户态的一次函数调用级别的操作,极快。而且线程管理策略可以由应用自己定制。
- 缺点有两条,都很致命:
- 一个线程阻塞,整个进程阻塞。 因为内核只认进程——某个线程发起阻塞式 I/O,内核把整个进程挂起,同进程内其他本可以运行的线程也跟着停了。
- 无法利用多核并行。 内核以进程为单位分配 CPU,一个进程同一时刻只会被分到一个核。
内核级线程(KLT):由内核管理和调度,每条线程在内核里有自己的 TCB。
- 优点:一个线程阻塞不影响同进程其他线程;能真正并行,内核可以把同一进程的多个线程分派到不同核上。
- 缺点:所有线程管理操作(创建、切换、同步)都要通过系统调用进内核,开销比 ULT 大。
疑问点:JVM 的线程属于哪一种模型
用户级线程与内核级线程在实际系统中如何划分? JVM 的平台线程是否为一对一模型,虚拟线程是否为多对多模型?
关联对照:JVM 平台线程与虚拟线程
该对应关系完全成立,且是三种模型最好的现实例证。
- Java 的平台线程(Platform Thread)确实是 1:1 的。现代 JVM(HotSpot)在 Linux 上,一个
java.lang.Thread直接对应一个内核线程(NPTL 的 pthread)。因此 Java 线程能真正并行、能被内核抢占,代价是创建线程需陷入内核并分配栈空间(默认 1MB),开销较大——这正是线程池存在的原因。- 虚拟线程(Virtual Thread,Loom)则是 M:N 模型。成千上万个虚拟线程由 JVM 在用户态调度到少数几个平台线程(载体线程)之上。创建成本极低,因为不需进入内核。
虚拟线程还完整复现了 ULT 的经典缺陷及其解法:虚拟线程若执行阻塞式的本地调用(例如在
synchronized块中阻塞,或调用某些旧式 JNI 方法),会将载体线程一并钉住(pinning),即”一个线程阻塞拖垮一片”的现代形态。JDK 的对策是把java.net等标准库的阻塞点改写为非阻塞并挂起虚拟线程,使载体线程得以转去执行其他任务——这与教材中 ULT 采用**非阻塞 I/O 加套管程序(jacketing)**缓解阻塞的思路一致。对应关系的失效处:教材中的 ULT 是纯用户态线程库、内核零参与;JVM 虚拟线程之下仍压着真实的内核线程池,属于工程上的混合实现。考试须按教材的纯粹模型作答。
多线程模型
ULT 和 KLT 的映射关系有三种(→ 多线程模型):
多对一:多个 ULT 映射到 1 个 KLT。就是纯用户级线程。任一线程阻塞则全进程阻塞,不能并行。
一对一:每个 ULT 对应 1 个 KLT。就是纯内核级线程。并发度最高,但创建线程开销大,且内核线程数量通常有上限。
多对多:m 个 ULT 映射到 n 个 KLT(m ≥ n)。兼顾两者——既有 ULT 的低开销,又因为有多个 KLT 而能并行、且单个线程阻塞不会拖垮全部。
判断题的固定套路:问”哪种模型下一个线程阻塞会导致整个进程阻塞”→ 多对一。
边界
只有内核级线程才是”调度的基本单位”
这句话要加限定。用户级线程对内核不可见,内核调度的仍是进程。 所以”线程是调度的基本单位”这个说法,严格来说只在内核级线程的语境下成立。
考题喜欢在这里设陷阱:问”用户级线程是不是处理机调度的单位”,答案是不是。
什么事件会导致用户级线程切换
疑问点:哪类事件才能引起用户级线程切换
- 下列选项中,可能导致用户级线程切换的事件是()。 A. 系统调用 B. I/O 请求 C. 异常处理 D. 线程同步
答案是 D 线程同步。
这道题的判据是:ULT 的切换由用户态线程库完成,必须是”在用户态就能决定并执行”的事件。
逐个看前三个为什么不是:
- A 系统调用:会陷入内核。内核根本不知道 ULT 的存在,它处理完系统调用后,只会把控制权还给这个进程,不会去切换进程内部的某条用户级线程。
- B I/O 请求:本身就是通过系统调用发出的,同上。而且在纯 ULT 模型下,I/O 阻塞的是整个进程——连切换的机会都没有了。
- C 异常处理:异常由硬件检测、内核处理,同样绕不开内核,也不涉及 ULT 的调度。
而 D 线程同步——比如线程 1 去获取一把用户态实现的锁,发现锁被占用,于是线程库让它让出 CPU,在用户态直接切到线程 2。全程不进内核,这正是 ULT 切换的典型场景。
一句话:能在用户态自己做主的,才可能引起 ULT 切换;一旦进内核,内核就只认进程。
线程和”为每个应用配一个线程”的措辞陷阱
疑问点:该选项错在何处,正确表述应当如何
C. 键盘驱动程序为每个正在运行的应用配备一个线程,用以响应该应用的键盘输入
该项为错误表述,需明确其错误所在及正确的说法。
这个选项错在把驱动程序的职责搞反了。
键盘是一台设备,它产生的中断是全局的。键盘驱动程序做的事情是:响应中断、把扫描码取出来、转换成字符、放进当前获得焦点的那个终端/应用的输入缓冲区。
驱动不会也不需要为每个应用各开一个线程。 那样做既浪费(大部分线程永远等不到属于自己的输入),也没有意义(中断只有一个源)。
正确的表述大致是:“键盘驱动程序响应键盘中断,将输入字符送入相应进程的输入缓冲区,由该进程自行读取。”
此类题目的通用识别方法:遇到”为每个 X 配备一个线程/进程”的描述,先追问是否真的需要这么多。 硬件设备通常只有一台,服务它的执行流也就只需一条。
对照速查
| 进程 | 线程 | |
|---|---|---|
| 资源分配单位 | ✅ | ❌ |
| 调度单位 | (引入线程前是) | ✅(指内核级线程) |
| 私有 | 地址空间、资源 | PC、寄存器组、栈、TCB |
| 共享 | — | 同进程的地址空间、文件 |
| 切换开销 | 大(换页表、TLB 失效) | 小(不换页表) |
| 用户级线程 ULT | 内核级线程 KLT | |
|---|---|---|
| 内核是否知情 | ❌ | ✅ |
| 切换是否进内核 | ❌ 快 | ✅ 慢 |
| 一个线程阻塞 | 全进程阻塞 | 只阻塞该线程 |
| 能否多核并行 | ❌ | ✅ |
| TCB 位置 | 用户空间 | 内核空间 |
| 模型 | 并行 | 单线程阻塞的影响 |
|---|---|---|
| 多对一 | ❌ | 全进程阻塞 |
| 一对一 | ✅ | 只影响该线程 |
| 多对多 | ✅ | 只影响该线程 |
考点
- 资源分配单位 = 进程,调度单位 = 线程(一切的起点)
- 线程私有的三样:PC、寄存器组、栈
- ULT 的两大缺陷:全进程阻塞、不能并行
- 可能导致 ULT 切换的是”线程同步”,系统调用/异常/IO 都不是
- 多对一模型下一个线程阻塞会拖垮整个进程
链接
- 🏠 返回总览:操作系统第 2 章:进程与线程总览
- ⬅️ 上一节:2.1.5 进程的通信
- ➡️ 下一节:2.2.1 调度的概念
- 📖 名词库:第 2 章名词库