主流的操作系统都提供了线程实现,Java语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经调用过start()方法且还未结束的java.lang.Thread类的实例就代表着一个线程。我们注意到Thread类与大部分的Java类库API有着显著差别,它的所有关键方法都被声明为 native。在Java类库API中,一个native方法往往就意味着这个方法没有使用或无法使用平台无关的手段来实现。
内核线程(KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量。
用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源分配到进程,那诸如“阻塞如何处理”“多处理器系统中如何将线程映射到其他处理器上”这类问题解决起来将会异常困难,甚至有些是不可能实现的。
因为使用用户线程实现的程序通常都比较复杂,除了有明确的需求外,一般的应用程序都不倾向使用用户线程。Java、Ruby等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如Golang、Erlang等。
混合实现
Java线程的实现
Java线程如何实现并不受Java虚拟机规范的约束,具体实现与具体Java虚拟机相关。
以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以HotSpot自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给底下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排给哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。
操作系统支持怎样的线程模型,在很大程度上会影响上面的Java虚拟机的线程是怎样映射的,这一点在不同的平台上很难达成一致,因此《Java虚拟机规范》中才不去限定Java线程需要使用哪种线程模型来实现。
线程调度是指系统为线程分配处理器使用权
的过程,调度主要方式有两种:
Java语言定义了6种线程状态,在任意一个时间点中,一个线程只能有且只有其中的一种状态,并且可以通过特定的方法在不同状态之间转换
正在执行
,也有可能正在等待着操作系统为它分配执行时间
。要等待被其他线程显式唤醒
。
由系统自动唤醒
。
1 : 1 的内核线程模型是如今Java虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高昂,系统能容纳的线程数量也很有限。
由于最初多数的用户线程是被设计成协同式调度(Cooperative Scheduling)的,所以它有了一个别名——“协程”(Coroutine)。又由于这时候的协程会完整地做调用栈的保护、恢复工作,所以今天也被称为“有栈协程”。
协程的主要优势是轻量,比传统内核线程要轻量得多。进行量化的话,那么如果不显式设置-Xss或-XX:ThreadStackSize,则在64位Linux上HotSpot的线程栈容量默认是1MB,此外内核数据结构还会额外消耗16KB内存。与之相对
的,一个协程的栈通常在几百个Byte到几KB之间,所以Java虚拟机里线程池容量达到两百就已经不算小了,而很多支持协程的应用中,同时并存的协程数量可数以十万计。
对于有栈协程,有一种特例实现名为纤程(Fiber),OpenJDK在2018年创建了Loom项目。从Oracle官方对“什么是纤程”的解释里可以看出,它就是一种典型的有栈协程。
Loom项目背后的意图是重新提供对用户线程的支持,但与过去的绿色线程不同,这些新功能不是为了取代当前基于操作系统的线程实现,而是会有两个并发编程模型在Java虚拟机中并存,可以在程序中同时使用。
Loom项目目前仍然在进行当中,还没有明确的发布日期,包含的内容也有被改动的可能。