1. 操作系统线程
无论使用何种编程语言编写多线程程序,最终都是通过调用操作系统的线程来执行任务。线程是CPU调度的最小执行单元。
线程有多种实现方式,常见的有:内核线程、用户线程、混合线程。
不同线程模型的主要区别在于线程的调度方不同,是操作系统还是虚拟机。
1.1 内核线程
由操作系统来负责多线程调度的多线程实现方式,叫做内核线程。
我们知道,**进程的地址空间分为内核空间和用户空间。**程序在内核空间执行时,CPU处于内核态,程序在用户空间执行时,CPU处于用户态。因此内核线程也叫做内核空间线程,或者内核态线程。
对于应用程序来说,其运行在用户空间,无法直接操作(创建、使用、销毁等)内核线程。因此,**操作系统暴露可以操作内核线程的系统调用,给应用程序使用。**因为系统调用比较底层,所以,大部分编程语言都对其进行封装,提供易用的线程接口,比如Linux中的pthread、C++中的std::thread等等。对于Java这种跨平台的语言来说,为了提供统一的线程操作接口,也会将操作系统提供的系统调用,封装为自己的线程类库。
内核线程模型,也叫做1:1模型。前面的1表示用户空间的一个线程,也就是在应用程序开发者眼中的一个线程,比如通过Java Thread创建的一个线程对象。后面的1表示内核空间的一个线程,也就是真正的线程。1:1模型指的就是:用户空间中的一个用户线程对应内核空间中的一个内核线程。
综上,应用程序运行在用户空间,通过系统调用才能实现对内核线程的操作。而系统调用会导致用户态和内核态的上下文切换,比较耗时。这是内核线程的一个弊端。
1.2 用户线程
为了解决内核线程存在的弊端(内核态和用户态的上下文切换),计算机科学家发明了用户线程。
类比内核线程,用户线程指的是线程的调度由虚拟机完成,而虚拟机本质上就是一个运行在用户空间的应用程序。
实现调度算法来调度线程的程序,叫做调度程序。用户线程的调度程序的实现思路,和内核线程的调度程序的实现思路基本一致。
实际上,用户线程只是一个外壳。从本质上来看,虚拟机执行三个线程,相当于轮询执行三段代码。所以应用程序操作用户线程(创建、使用、销毁等),都是在用户空间完成的,完全不需要操作系统内核的参与,这样就避免了系统调用带来的用户态和内核态的上下文切换。
不过,用户线程需要有专门的结构来记录上下文信息。除此之外,虚拟机也需要为每个用户线程维护独立的函数调用栈。
用户线程也叫做M:1线程模型。其中M表示M个用户线程,1表示1个内核线程。当虚拟机在运行时,操作系统会为其创建进程,而且是单线程的进程,这里的单线程指的是内核线程,即一个内核线程对应多个用户线程。
操作系统线程调度算法调度的是内核线程,为内核线程之间公平地分配时间片。不管虚拟机中创建多少个用户线程,它们都只能共享一个内核线程的CPU时间片。因此用户线程无法利用多核优势。
除此之外,用户线程在使用上还有另外的限制。在用户线程中,我们无法使用阻塞模式的系统调用,比如read()、write()等阻塞IO系统调用。在内核线程中,当我们调用read()、write()等阻塞IO系统调用时,操作系统会让当前线程让出时间片,切换为其他线程执行。对于用户线程来说,当一个用户线程中的代码调用了阻塞IO系统调用时,对应的内核线程,就会被操作系统调度让出时间片,直到IO读写完成才会放入就绪队列。也就是说,只要一个用户线程阻塞了,其他用户线程也无法工作了。
解决这个问题的办法是,在用户线程中不要使用阻塞模式的系统调用,我们可以使用非阻塞的系统调用,内核线程在执行这类非阻塞的系统调用时,不需要让出时间片,可以继续执行后续的代码。
当然,相对于阻塞模式的系统调用,非阻塞模式的系统调用使用起来很不方便。比如调用非阻塞的write()系统调用,应用程序需要轮询查看是否写入完成。为了解决这个问题,一般支持用户线程的编程语言,会使用非阻塞函数模拟实现阻塞函数。在用法上,让程序员感知好像是在使用阻塞函数,实际上,底层使用的是非阻塞的系统调用来实现的。
1.3 混合线程
用户线程的优缺点
优点:避免了使用内核线程导致的内核态和用户态之间的上下文切换。
缺点:(1)一个进程内的用户线程无法利用多核并行运行;(2)一个用户线程调用阻塞系统调用会阻塞一个进程中的所有用户线程。
为了解决用户线程的缺点,计算机科学家发明了混合线程,又叫做M:N线程模型。
**M:N线程模型表示一个进程中的M个用户线程对应N个内核线程,M一般大于N。**如果M等于N,那就退化成了1:1线程模型。如果M小于N,那么多余的内核线程就浪费掉。如果应用程序创建M个用户线程,那么虚拟机就会使用操作系统提供的系统调用,创建N个内核线程来服务这M个用户线程。M个用户线程并不会绑定在一个内核线程上,因此,一个用户线程阻塞并不会导致所有的用户线程阻塞。同时,M个用户线程分散在N个内核线程上,不同的用户线程可以分散在不同的CPU上执行,也就利用到了计算机多核的优势。
2. Java线程与操作系统线程的关系
Java线程有两种实现方式,一种叫Green Thread,一种叫Native Thread。
实际上,Green Thread就是用户线程模型,也就是M:1线程模型。Green Thread实际上只存在于早期jdk版本中,在JDK1.3中便已经废弃,被Native Thread取而代之。
Native Thread实际上就是内核线程模型,也就是1:1线程模型。Java提供的线程库,只不过是对操作系统提供的操作内核线程的系统调用的二次封装。线程的调度由操作系统来完成,因此,Java线程库实现起来非常简单。
每个操作系统的内核线程实现都有细微差别,比如线程的状态定义、线程的优先级划分等等都有可能不同。Java作为跨平台编程语言,需要提供统一编程接口。为了封装各个操作系统中线程实现的差别,Java线程库定义了自己的线程状态和优先级,以及和各个操作系统中线程状态和优先级的映射关系。