Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • ☕ Understanding the JVM
    • Part 2 - 自动内存管理

      • Chapter 2 - Java 内存区域与内存溢出异常
      • Chapter 3.1-3.2 - 概述 && 对象已死
      • Chapter 3.3-3.4 - 垃圾收集算法 && HotSpot 的算法实现细节
      • Chapter 3.5 - 经典垃圾收集器
      • Chapter 3.6 - 低延迟垃圾收集器
    • Part 3 - 虚拟机执行子系统

      • Chapter 6 - 类文件结构
      • Chapter 7 - 虚拟机类加载机制
      • Chapter 8.1-8.2 - 运行时栈帧结构
      • Chapter 8.3 - 函数调用
      • Chapter 8.5 - 基于栈的字节码解释执行引擎
      • Chapter 9.2 - Tomcat: 正统的类加载器架构
    • Part 4 - 程序编译与代码优化

      • Chapter 10 - 前端编译与优化
      • Chapter 11.2 - 即时编译器
      • Chapter 11.3-11.4 - 提前编译器 && 编译器优化技术
    • Part 5 - 高效并发

      • Chapter 12.3 - Java 内存模型
      • Chapter 12.4-12.5 - Java 线程与协程
      • Chapter 13 - 线程安全与锁优化

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 线程的缺陷有:

  1. 切换、调度成本高昂
  2. 系统容纳的线程数量有限

在请求执行时间很短、数量很多的前提下,线程切换的开销甚至可能会接近于计算本身的开销,造成严重浪费。传统的 Java Web 服务器的线程池容量通常在几十个到两百,当数以万计的请求灌入时,系统即使能处理得过来,切换损耗也是相当大的。

12.5.2 协程的复苏

内核线程的调度成本主要来自于用户态和核心态之间的状态切换,其中的开销主要来自于响应中断、保护和恢复现场的成本。如果把保护、恢复现场和调度的工作从 OS 交到程序员手上,就可以玩很多花样来缩减开销:用户程序自行模拟多线程演化为用户线程。由于大多数用户线程被设计为协同式调度,因此被称为 协程 (Coroutine)。

协程的优势在于轻量,比传统的内核线程轻量得多。在 JVM 中,线程池达到 200 就已经不算小了,而很多支持协程的应用中,同时并存的协程数量数以十万计。协程的局限在于,需要在应用层面实现的内容特别多 (调用栈、调度器)。

12.5.3 Java 的解决方案

OpenJDK 在 2018 年创建的 Loom 项目,Java 引入了 纤程 (Fiber) 的概念,重新对用户线程提供支持,是日后会与目前线程模型平行的新并发编程机制。日后会有两个并发编程模型在 JVM 中并存,可以在程序中同时使用。在新并发模型下,使用纤程并发的代码会被分为两部分:

  • 执行:维护线程,保护、恢复上下文状态
  • 调度:编排要执行的代码顺序

分离的好处是,用户可以自行控制任意部分。Java 中现有的调度器也可以被直接重用。

Edit this page on GitHub
Prev
Chapter 12.3 - Java 内存模型
Next
Chapter 13 - 线程安全与锁优化