Java线程和操作系统线程的关系解读
作者:CringKong
1.操作系统线程模型
1.1 线程实现在用户空间下
当线程在用户空间下实现时,操作系统对线程的存在一无所知,操作系统只能看到进程,而不能看到线程。所有的线程都是在用户空间实现。
在操作系统看来,每一个进程只有一个线程。过去的操作系统大部分是这种实现方式,这种方式的好处之一就是即使操作系统不支持线程,也可以通过库函数来支持线程。
我们换一种通俗的方式来讲解这段话,首先就是在这在模型下,程序员需要自己实现线程的数据结构、创建销毁和调度维护。也就相当于需要实现一个自己的线程调度内核,而同时这些线程运行在操作系统的一个进程内,最后操作系统直接对进程进行调度。
这样做有一些优点,首先就是确实在操作系统中实现了真实的多线程,其次就是线程的调度只是在用户态,减少了操作系统从内核态到用户态的切换开销。
当然缺点也很明显:
这种模式最致命的缺点也是由于操作系统不知道线程的存在,因此当一个进程中的某一个线程进行系统调用时,比如缺页中断而导致线程阻塞,此时操作系统会阻塞整个进程,即使这个进程中其它线程还在工作。
还有一个问题是假如进程中一个线程长时间不释放CPU,因为用户空间并没有时钟中断机制,会导致此进程中的其它线程得不到CPU而持续等待。
1.2 线程实现在操作系统内核中
内核线程就是直接由操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。
每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。
通俗的将就是,程序员直接使用操作系统中已经实现的线程,而线程的创建、销毁、调度和维护,都是靠操作系统(准确的说是内核)来实现,程序员只需要使用系统调用,而不需要自己设计线程的调度算法和线程对CPU资源的抢占使用。
而不得提到的就是,Linux下的轻量级进程,因为Linux并不像Windows那样,真正的实现了线程的数据结构,而是直接采用了和系统中进程一样的实现方式。
目前的Linux已经基于NPTL实现了更符合POSIX标准的线程,之前我学习到的LinuxThreads早已经被替代,这里为自己的不求甚解反省。
轻量级进程(LWP)是建立在内核之上并由内核支持的用户线程,它是内核线程的高度抽象,每一个轻量级进程都与一个特定的内核线程关联。内核线程只能由内核管理并像普通进程一样被调度。
Linux中程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口–轻量级进程,轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。 这种轻量级进程与内核线程之间1:1的关系称为一对一的线程模型。
这里要说明的是,一对一的线程模型是一种概念,Linux是一对一的线程模型,上面的表述太绕了,现在的Linux中线程已经的被更优雅的实现了。
PS:其实Linux中的pthread库就是调用了轻量级线程接口,来在操作系统中创建一个内核线程。
Pthread库目前也是基于NPTL实现,已经是一种符合POSIX标准的线程模型了,Linux摆脱了原来LWP的阴影。
1.3使用用户线程加轻量级进程混合实现
在这种混合实现下,即存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。
而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,大大降低了整个进程被完全阻塞的风险。
在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系:
明白了前面两种模型,就应该很好理解这种线程模型了,但实际上现在主流的操作系统已经不太常用这种线程模型了。
2019.3.26更新:
目前来说,作为异步回调以外的另一种解决方案,这种m:n的线程模型可以说大有可为,Golang的协程就是使用了这种模型,在用户态,协程能快速的切换,避免了线程调度的CPU开销问题,协程相当于线程的线程。
2.Java线程
2.1 Java线程在操作系统上本质
Java线程在JDK1.2之前,是基于称为“绿色线程”(Green Threads)的用户线程实现的,而在JDK1.2中,线程模型替换为基于操作系统原生线程模型来实现。
因此,在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定Java线程需要使用哪种线程模型来实现。
线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编码和运行过程来说,这些差异都是透明的。
也就说JDK1.2之前,
**程序员们为JVM开发了自己的一个线程调度内核,而到操作系统层面就是用户空间内的线程实现。
**而到了JDK1.2及以后,JVM选择了更加稳健且方便使用的操作系统原生的线程模型,通过系统调用,将程序的线程交给了操作系统内核进行调度
对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的。
也就是说,现在的Java中线程的本质,其实就是操作系统中的线程,Linux下是基于pthread
库实现的轻量级进程,Windows下是原生的系统Win32 API
提供系统调用从而实现多线程。
2.2 Java中的线程
特别注意:这些线程的状态时JVM中的线程状态!不是操作系统中的线程状态。
2.2.1 操作系统中的进程(线程)状态**(区分和JVM中的线程状态)**
这里需要着重解释一点,在现在的操作系统中,因为线程依旧被视为轻量级进程,所以操作系统中线程的状态实际上和进程状态是一致的模型。
2.2.2 操作系统中线程和Java线程状态的关系:
从实际意义上来讲,操作系统中的线程除去new
和terminated
状态,一个线程真实存在的状态,只有:
ready
:表示线程已经被创建,正在等待系统调度分配CPU使用权。running
:表示线程获得了CPU使用权,正在进行运算waiting
:表示线程等待(或者说挂起),让出CPU资源给其他线程使用
为什么除去new
和terminated
状态?
是因为这两种状态实际上并不存在于线程运行中,所以也没什么实际讨论的意义。
对于Java中的线程状态:
无论是Timed Waiting
,Waiting
还是Blocked
,对应的都是操作系统线程的**waiting
(等待**)状态。
而Runnable
状态,则对应了操作系统中的ready
和running
状态。
而对不同的操作系统,由于本身设计思路不一样,对于线程的设计也存在种种差异,所以JVM在设计上,就已经声明:
虚拟机中的线程状态,不反应任何操作系统线程状态
所以我上面说的那么多,只是作为理解模型,Java线程和操作系统线程,实际上同根同源,但又相差甚远。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。