读《深入理解Java虚拟机》第三版,周志明著。
我们知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调用分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度。目前线程是 Java 里面进行处理器资源调度的最基本单位,不过如果日后 Loom 项目(https://wiki.openjdk.java.net/display/loom/Main)能成功为 Java 引入纤程(Fiber)的话,可能就会改变这一点。
主流的操作系统都提供了线程实现,Java 语言也提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经调用过 start()
方法且还未结束的 java.lang.Thread
类的实例就代表着一个线程。我们注意到 Thread 类与大部分的 Java 类库 API 有着显著的差别,它的所有关键方法都被声明为 Native。那么意味着线程的实现无法使用平台无关的手段。
以一个通用的应用程序的角度来看看线程是如何实现的。
一、实现线程的主要方式
实现线程主要有三种方式:
- 使用内核线程实现( 1:1 实现),
- 使用用户线程实现( 1:N 实现),
- 使用用户线程加轻量级进程混合实现( N:M 实现)。JAVA
1.1、内核线程实现
使用内核线程实现的方式也被称为 1:1 的实现。
内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核(Kernel,以下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就称为多线程内核(Multi-Threads Kernel)。
程序一般不会直接使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。
这种轻量级进程与内核线程之间 1:1 的关系称为一对一的线程模型。
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。
轻量级进程也具有它的局限性:
- 首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
- 其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
1.2、用户线程实现
使用用户线程实现的方式被称为 1:N 的实现。
广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程(User Thread,UT)的一种,因此从这个定义上看,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作系统都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点。
而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快且低消耗的,也能支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。
这种进程与用户线程之间 1:N 的关系称为一对多的线程模型。
用户线程:
- 优势在于不需要系统内核支援。
- 劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。
线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如 “ 阻塞如何处理 ” 、“ 多处理器系统中如何讲线程映射到其他处理器上 ” 这类问题解决起来将会异常困难,甚至有些是不可能实现的。因此使用用户线程实现的程序通常都比较复杂,除了有明确的需求外(譬如以前在不支持多线程的操作系统中的多线程程序、需要支持大规模线程数量的应用),一般的应用程序都不倾向使用用户线程。
Java、Ruby 等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如 Golang、Erlang 等,使得用户线程的使用率有所回升。
1.3、混合实现
线程除了依赖内核实现和完全由用户程序自己实现以外,还由一种将内核线程与用户线程一起使用的实现方式,被称为 N:M 实现。
在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。
在这种混合模式中,用户线程与轻量级进程的数量比是不定的,是 N:M 的关系。
许多 UNIX 系列的操作系统,如 Solaris、HP-UX 等都提供了 M:N 的线程模型实现。在这些操作系统上的应用也相对容易应用 M:N 的线程模型。
二、Java 线程的实现
Java 线程如何实现并不受 Java 虚拟机规范的约束,这是一个与具体虚拟机相关的话题。Java 线程在早期的 Classic 虚拟机上(JDK 1.2 以前),是基于一个被称为 “ 绿色线程 ”(Green Threads)的用户线程实现的,但是 JDK 1.3 起,“ 主流 ” 平台上的 “ 主流 ” 商用 Java 虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用 1:1 的线程模型。
以 HotSpot 为例,它的每一个 Java 线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以 HotSpot 自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以:
- 何时冻结或唤醒线程
- 该给线程分配多线处理器执行时间
- 该把线程安排给那个处理器核心去执行
- 等
都是由操作系统完成的,也都是由操作系统全权决定的。
前面强调是两个 “ 主流 ”,就是说明肯定还有例外的情况,这里举两个比较著名的例子:
- 一个是用于 Java ME 的 CLDC HotSpot Implementation(CLDC-HI)。它同时支持两种线程模型,默认使用 1:N 的用户线程实现的线程模型,所有 Java 线程都映射到一个内核线程上;不过它也可以使用另一个特殊的混合模型,Java 线程仍然全部映射到一个内核线程上,但是当 Java 线程要执行一个阻塞的调用时,CLDC-HI 会为该调用单独开一个内核线程,并且调度执行其他 Java 线程,等到那个阻塞调用完成之后再重新调度之前的 Java 线程继续执行。
- 另外一个例子是在 Solaris 平台的HotSpot 虚拟机,由于操作系统的线程特性本来就可以同时支持 1:1(通过 Bound Threads 或 Alternet Libthreads实现)及 N:M(通过 LWP / Thread Based Synchronization 实现)的线程模型,因此 Solaris 版的 HotSpot 也对应提供了两个平台专有的虚拟机参数,即 -XX:UseLWPSynchronization(默认) 和 -XX:UseBoundThreads 来明确指定虚拟机只用哪种线程模型。
操作系统支持怎样的线程模型,在很大程度上会影响上面的 Java 虚拟机的线程是怎样映射的,这一点在不同的平台上很难达成一致,因此《Java虚拟机规范》中才不去限定 Java 线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对 Java 程序的编码和运行过程来说,这些差异都是完全透明的。