29、线程模型
上一节我们讲到操作系统的线程状态,细心的同学可能已经发现,操作系统的线程状态,跟 Java 中的线程状态并不吻合,这是为什么呢?
Java 线程跟操作系统线程有什么关系呢?除此之外,我们还常听说 Java 线程的两种实现方式,一种叫 Green Thread,一种叫 Native Thread,这两种实现方式有什么区别呢?
带着这些问题,我们来学习本节的内容:线程模型
1、线程模型概述
所谓线程模型指的就是线程的实现方式,线程有各种实现方式,常见的有:内核线程、用户线程、混合线程
除此之外,你或许还听说过其他线程模型:1:1 线程模型、M:1 线程模型、M:N 线程模型,以及刚刚提到的 Green Thread、Native Thread,甚至还听说过协程
实际上,这些都是对线程实现方式的不同称谓罢了,它们之间的对应关系如下图所示
2、线程模型一:内核线程
2.1、讲解
上一节我们讲到线程调度,不同线程模型的主要区别在于:线程调度是由谁来完成,是操作系统内核还是虚拟机
由操作系统内核来负责多线程调度的多线程实现方式,就叫做内核线程
前面在讲解 Java IO 的时候我们提到,进程地址空间分为用户空间和内核空间,程序在用户空间执行时,CPU 处于用户态,程序在内核空间执行时,CPU 处于内核态
而调度程序运行在内核空间,CPU 处于内核态,因此内核线程,也叫做内核空间线程或内核态线程
对于应用程序来说,其运行在用户空间,是无法直接操作(创建、使用、销毁等)内核线程的,因此操作系统暴露操作线程的系统调用,给应用程序使用
因为系统调用比较底层,所以大部分编程语言都对其进行封装,提供易用的线程接口,比如 Linux 中的 pthread、C++ 中的 std::thread 等等
对于 Java 这种跨平台的语言来说,为了提供统一的线程操作接口,也必然会将操作系统提供的系统调用,封装为自己的线程类库
内核线程,也叫做 1:1 模型
- 前面的 1 表示用户空间的一个线程,也就是在应用程序开发者眼里的一个线程,比如通过 Java Thread 创建的一个线程对象
- 后面的 1 表示内核空间的一个线程,也就是真正的线程
- 1:1 模型指的就是:一个应用程序中的线程对应一个内核线程
2.1、优缺点
从上述讲解我们发现,应用程序需要通过系统调用,才能实现对内核线程的操作
而前面讲过,系统调用会导致用户态和内核态的上下文切换,比较耗时,这是内核线程的一个弊端
3、线程模型二:用户线程
3.1、讲解
为了解决内核线程的弊端(系统调用导致上下文切换),于是计算机科学家们就发明了用户线程
类比内核线程,用户线程指的是线程的调度由虚拟机完成
因为虚拟机本质上就是一个应用程序,运行在用户空间,所以用户线程也叫做用户空间线程或用户态线程,实际上我们常听到的协程,也就是用户线程
实现调度算法来调度线程的程序,我们叫做调度程序,用户线程的调度程序的实现思路,跟内核线程的调度程序的实现思路,基本上是一致的
虚拟机中内嵌一个调度程序,如果运行虚拟机上的应用程序创建了 3 个用户线程,那么虚拟机在运行的过程中,当获得 CPU 时间片之后
- 方式一:会分为 3 个小的时间片,分别运行这三个用户线程对应的代码
- 方式二:把时间片全部用来执行一个用户线程的代码,等到虚拟机再次获得 CPU 时间片之后,再把时间片全部用来执行另一个用户线程的代码
实际上,用户线程只是一个华丽的外壳,抛开外壳,从本质上看,虚拟执行三个线程,就相当于轮询执行三段代码(每个线程对应一段代码)
所以应用程序操作用户线程(创建、使用、销毁等),都是在用户空间完成的,完全不需要操作系统内核的参与,这样就避免了系统调用带来的用户态和内核态的上下文切换
不过用户线程也需要有专门的结构来记录上下文信息
虚拟机在执行某个用户线程对应的代码时,如果分配给这个用户线程的时间片用完了,那么就需要保存这个用户线程的上下文,以便之后再次获得时间片之后重新继续执行
除此之外,虚拟机也需要为每个用户线程维护独立的栈(也就是函数调用栈)
用户线程也叫做 M:1 线程模型:其中 M 表示 M 个用户线程,1 表示 1 个内核线程
我们知道,虚拟机本质上也是程序,在运行时,操作系统会为其创建进程,并且是单线程(这里的线程指的是内核线程)的进程,如下图所示
3.2、优缺点
操作系统线程调度算法调度的是内核线程,为内核线程之间公平地分配时间片
不管虚拟机中创建多少个用户线程,它们都只能共享一个内核线程的 CPU 时间片,因此用户线程无法利用多核优势
即便一台计算机上只运行一个虚拟机,虚拟机上的多个用户线程也只能排队使用一个 CPU 资源,其他 CPU 资源都在白白浪费,这就是用户线程的相对于内核线程的弊端
除此之外,用户线程在使用上还有另外的限制,在用户线程中,我们无法使用阻塞模式的系统调用,比如 read()、write() 等阻塞 I / O 系统调用
- 在内核线程中,当我们调用 read()、write() 等阻塞 I / O 系统调用时,操作系统会让当前线程会让出时间片,切换为其他线程执行
- 对于用户线程来说,当一个用户线程中的代码调用了 read()、write() 等阻塞 I / O 系统调用时,对应的内核线程,就会被操作操作系统调度让出时间片
直到 I / O 读写完成才会放入就绪队列,重新排队等待分配时间片,也就是说,只要一个用户线程阻塞了,其他用户线程也无法工作了
解决这个问题的办法就是,在用户线程中不要使用阻塞模式的系统调用,比如读写 I / O,我们可以使用非阻塞的 read()、write() 系统调用
操作系统一般都会提供这类非阻塞的 I / O 系统调用,并且内核线程在执行这类非阻塞的 I / O 系统调用时,不需要让出时间片,可以继续执行后续的代码
当然相对于阻塞模式的系统调用,非阻塞模式的系统调用使用起来很不方便,比如调用非阻塞的 write() 系统调用,应用程序需要轮询查看是否写入完成
为了解决这个问题,一般支持用户线程的编程语言,会使用 "非阻塞函数" 模拟实现 "阻塞函数"
在用法上,让程序员感知好像是在使用 "阻塞函数",实际上底层使用的是 "非阻塞的系统调用" 来实现的
4、线程模型三:混合线程
用户线程虽然可以避免使用内核线程导致的内核态和用户态的上下文切换,但是用户线程也存在弊端
比如:一个进程内的用户线程无法利用多核并行运行,以及一个用户线程调用阻塞系统调用会阻塞一个进程中的所有用户线程
为了解决这些问题,计算机科学家又发明了混合线程,混合线程又叫做 M:N 线程模型
M:N 线程模型表示一个进程中的 M 个用户线程对应 N 个内核线程,M 一般大于 N
- 如果 M 等于 N,那就退化成了 1:1 线程模型
- 如果 M 小于 N,那么多余的内核线程会浪费掉
如果应用程序创建 M 个用户线程,那么虚拟机就会使用操作系统提供的系统调用,创建 N 个内核线程来服务这 M 个用户线程
M 个用户线程并不会绑定在一个内核线程上,因此一个用户线程阻塞并不会导致所有的用户线程阻塞
同时 M 个用户线程分散在 N 个内核线程上,不同的用户线程可以分散在不同的 CPU 上执行,也就利用到了计算机多核的优势
相对于 1:1 线程模型和 M:1 线程模型,M:N 线程模型实现起来也比较复杂
刚刚我们讲到,使用 M:1 模型实现的用户线程,也称为协程
不严格的讲,使用 M:N 模型实现的线程,也可以称为是协程,Go 语言中的线程就是基于 M:N 模型来实现的,有时候也被叫做 Go 协程
5、Java 线程的实现原理
Java 线程有两种实现方法,一种叫 Green Thread,一种叫 Native Thread
实际上,Green Thread 就是 "用户线程" 模型,也就是 M:1 线程模型
之所以叫 Green Thread,是因为开发这个项目的团队名称叫 Green Team,因此他们把开发的线程库命名为 Green Thread
实际上 Green Thread 只存在于早期 JDK 版本中(JDK 1.1 和 JDK 1.2),在 JDK 1.3 中便已经废弃,取而代之是 Native Thread
Native Thread 实际上就是 "内核线程" 模型,也就是 1:1 线程模型
Java 提供的线程库,只不过是对操作系统提供的 "操作内核线程的系统调用" 的二次封装,线程的调度由操作系统来完成,因此 Java 线程库实现起来非常简单
每个操作系统的内核线程实现都有细微差别,比如线程的状态定义、线程的优先级划分等等都有可能不同,Java 作为跨平台编程语言,需要提供统一编程接口
为了封装各个操作系统中线程实现的差别,Java 线程库定义了自己的线程状态和优先级,并且定义了线程状态和优先级跟各个操作系统中线程状态和优先级的映射关系
上一节中我们讲到,操作系统中基本的线程状态有 5 个,分别是:NEW、READY、RUNNING、WAITING、TERMINATED
而 Java 中定义了 6 个线程状态,分别是:NEW、RUNNABLE、WAITING、TIMED_WAITING、BLOCKED、TERMINATED
关于 Java 中各个线程状态的含义、触发条件、跟操作系统线程状态的映射关系,我们在后面章节中详细讲解
除此之外,Java 线程中定义了 10 个级别的线程优先级(从 1 到 10)
跟线程状态类似,Java 定义的线程优先级跟操作系统定义的线程优先级也并非一一对应,例如:Linux 优先级有 140 个,Window 线程优先级有 7 个
6、课后思考题
用户线程模型,也就是 M:1 线程模型,因为无法利用多核优势,是不是就一无是处呢?
不完全是这样,虽然 M:1 线程模型无法充分利用多核优势,但它也有一些优点
- 简单易用:M:1 线程模型是最简单的线程模型之一,容易实现和理解
- 轻量级:由于用户线程是在用户空间中运行的,所以它们的创建和切换开销比内核线程小得多
- 灵活性高:用户线程可以灵活地控制线程的创建、销毁、调度等,而内核线程通常需要依赖操作系统的调度器
- 易于移植:由于 M:1 线程模型不依赖于任何特定的操作系统实现,因此它可以轻松地移植到不同的操作系统和硬件平台上
本节讲到了 1:1 线程模型、M:1 线程模型、M:N 线程模型,那么是否可以设计 1:N 线程模型呢?为什么?
可以,我们需要设计实现用户线程库,支持向同一个用户线程中提交多个并行执行的任务,这样就可以由多个内核线程并行执行
1:N 线程模型是指一个用户线程可以绑定多个内核线程,这样就可以充分利用多核优势,提高系统的并发性能和吞吐量
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17475154.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步