Chapter 12.4-12.5 - Java 线程与协程
Created by : Mr Dk.
2020 / 02 / 03 22:24
Ningbo, Zhejiang, China
12.4 Java 与线程
12.4.1 线程的实现
线程是比进程更轻量级的调度执行单位,可以把进程的 资源分配 和 执行调度 分开。各线程既可以共享进程资源,又可以独立调度。
线程是 Java 中进行 CPU 资源调度的基本单位,Java 提供了在不同硬件和 OS 平台下对线程的统一处理。Thread
类的所有关键函数都被声明为 Native,意味着没有使用或无法使用平台无关的手段实现。
12.4.1.1 内核线程 (Kernel-Level Thread, KLT) 实现
直接由 OS 内核支持的线程,由内核来完成线程切换。内核通过操纵调度器对线程进行调度,并映射到各个 CPU 上。程序使用内核线程的高级接口 - 轻量级进程 (Light Weight Process, LWP),每个轻量级进程都由一个内核线程支持 - 1:1 线程模型。
由于其基于内核线程实现,线程的各种操作都需要进行系统调用。涉及到用户态与核心态的切换,代价较高。轻量级进程要消耗一定的内核资源,因此一个系统支持的轻量级进程数量是有限的。
12.4.1.2 用户线程 (User Thread, UT) 实现
1:N 线程模型,完全建立在用户空间的线程库上,OS 内核不能感知用户线程的存在。线程的建立、同步、销毁、调度完全在用户态完成,不需要切换到内核态,因此操作快速且高效,并能够支持规模更大的线程数量。但所有的线程操作都需要用户程序自己处理,较为复杂,Java 目前放弃使用它。
12.4.1.3 混合实现
N:M 线程模型,既存在用户线程,又存在轻量级进程:
- 用户线程完全建立在用户空间中
- OS 支持的轻量级进程是用户线程和内核线程之间的桥梁
由此,可以使用内核提供的线程调度功能和 CPU 映射,用户线程的系统调用通过轻量级进程完成,降低了整个进程被完全阻塞的风险。许多 UNIX 系列的 OS 都提供了 N:M 的线程模型实现。
12.4.1.4 Java 线程的实现
Java 的线程实现不受 JVM 规范约束,是一个与具体 JVM 相关的话题。目前,主流平台上的主流商用 JVM 普遍使用基于 OS 原生的线程模型实现,即 1:1 线程模型。HotSpot 的每个 Java 线程都被映射到 OS 原生线程,且中间没有额外的间接结构。因此 HotSpot 自己不会干涉线程调度,所有的调度全权由 OS 决定。当然也有非主流的情况,OS 支持什么样的线程模型,很大程度上会影响 JVM 的线程如何映射。对于 Java 程序的编码和运行来说,这些差异都是透明的。
12.4.2 Java 线程调度
线程调度指系统为线程分配 CPU 使用权的过程,分为:
- 协同式线程调度 (Cooperative Threads-Scheduling)
- 线程执行时间由线程本身控制
- 线程把自身工作执行完后,主动通知系统切换到另一个线程
- 实现简单,一般没有线程同步的问题
- 线程执行时间不受控制
- 抢占式线程调度 (Preemptive Threads-Scheduling)
- 每个线程由系统来分配执行时间
- 线程的执行时间可控
虽然 Java 的线程调度由 OS 自动完成,但依然可以建议 OS 给某些线程多分配一些时间。Java 设置了 10 个级别的线程优先级,当两个线程同时处于 Ready 状态时,优先级越高的线程越容易被选择执行。但是线程优先级并不是一项稳定的调节手段:
- OS 提供的线程优先级与 Java 的 10 个优先级通常不能一一对应
- 对于优先级数比 Java 小的 OS,不得不出现几个 Java 线程优先级对应于同一个 OS 优先级的情况
另外,也不能过分依赖于优先级。因为在有的 OS 中,优先级可能会被系统自行改变。比如 Windows 中有 优先级推进器 (Priority Boosting) 的功能:当 OS 发现一个线程被执行地特别频繁时,会越过优先级为其分配时间。
12.4.3 状态转换
Java 定义了六种线程状态:
- 新建 (New):创建后尚未启动的线程状态
- 运行 (Runnable):running 或 ready
- 无限期等待 (Waiting):不会被分配 CPU 时间,等待被其它线程 显式唤醒
- 有限期等待 (Timed Waiting):不会被分配 CPU 时间,在一定时间后会由 OS 自动唤醒
- 阻塞 (Blocked):阻塞等待获取排它锁 (等待释放) / 等待一段时间,或唤醒动作发生
- 结束 (Terminated)
12.5 Java 与协程
12.5.1 内核线程的局限
随着业务量的增长,一次对外部业务请求的响应,需要分布在不同机器上的大量服务共同协作来实现。减少单个服务复杂度、增加复用性,增加了服务的数量,缩短了留给每个服务的响应时间。要求每个服务需要在极短的时间内完成,也要求每个服务提供者都要能同时处理数量更庞大的请求。
但是映射到 OS 线程的缺陷有:
- 切换、调度成本高昂
- 系统容纳的线程数量有限
在请求执行时间很短、数量很多的前提下,线程切换的开销甚至可能会接近于计算本身的开销,造成严重浪费。传统的 Java Web 服务器的线程池容量通常在几十个到两百,当数以万计的请求灌入时,系统即使能处理得过来,切换损耗也是相当大的。
12.5.2 协程的复苏
内核线程的调度成本主要来自于用户态和核心态之间的状态切换,其中的开销主要来自于响应中断、保护和恢复现场的成本。如果把保护、恢复现场和调度的工作从 OS 交到程序员手上,就可以玩很多花样来缩减开销:用户程序自行模拟多线程演化为用户线程。由于大多数用户线程被设计为协同式调度,因此被称为 协程 (Coroutine)。
协程的优势在于轻量,比传统的内核线程轻量得多。在 JVM 中,线程池达到 200 就已经不算小了,而很多支持协程的应用中,同时并存的协程数量数以十万计。协程的局限在于,需要在应用层面实现的内容特别多 (调用栈、调度器)。
12.5.3 Java 的解决方案
OpenJDK 在 2018 年创建的 Loom 项目,Java 引入了 纤程 (Fiber) 的概念,重新对用户线程提供支持,是日后会与目前线程模型平行的新并发编程机制。日后会有两个并发编程模型在 JVM 中并存,可以在程序中同时使用。在新并发模型下,使用纤程并发的代码会被分为两部分:
- 执行:维护线程,保护、恢复上下文状态
- 调度:编排要执行的代码顺序
分离的好处是,用户可以自行控制任意部分。Java 中现有的调度器也可以被直接重用。