0%

Java 虚拟机 1:线程模型及Java的线程模型

一、进程、线程

在现代操作系统中,线程是处理器调度和分配的基本单位,进程则作为资源(内存地址、文件 I/O 等)拥有的基本单位。线程是进程内部的一个执行单元。 每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

二、用户态和内核态

由于进程的调度以及系统资源的分配是离不开操作系统的,所以学习谈线程设计之前,有必要先看下操作系统的体系结构,以 Unix/Linux 的体系架构为例。

因为操作系统的资源是有限的,如果访问资源的操作过多,必然会消耗过多的资源,而且如果不对这些操作加以区分,很可能造成资源访问的冲突。所以,为了减少有限资源的访问和使用冲突,Unix/Linux 的设计哲学之一就是:对不同的操作赋予不同的执行等级(有多大能力做多大的事),用户态(User Mode)和内核态(Kernel Mode)。

运行于用户态的进程可以执行的操作和访问的资源都会受到极大的限制,而运行在内核态的进程则可以执行任何操作并且在资源的使用上没有限制。很多程序开始时运行于用户态,但在执行的过程中有时候会需要切换到内核态执行,常见如以下三种:

1.系统调用:发一些操作需要在内核权限下才能执行,这就涉及到一个从用户态切换到内核态的过程。比如 printf(),调用的是 wirte()系统调用来输出字符串,等等。

2.不可知的异常事件:就会触发从当前用户态执行的进程转向内核态执行相关的异常事件。

3.外围设备的中断:CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序,如果先前执行的指令是在用户态下,则自然就发生从用户态到内核态的转换。

而在用户态和内核态之间的上下文切换,不可避免的会产生一定的开销,这也是线程设计中必须考虑到的点。

三、线程设计的三个难点

1、在 CPU 密集型任务、I/O 密集型任务以及充分利用多核 CPU 提升程序性能上找到一个平衡点。
2、尽可能支持规模更大的线程数量。
3、减少线程在用户态(User Mode)和内核态(Kernel Mode)中切换带来的开销。

四、三种不同的线程模型

基本概念

1.用户线程与内核级线程

线程的实现可以分为两类:用户级线程(User-LevelThread, ULT)和内核级线程(Kemel-LevelThread, KLT)。用户线程由用户代码支持,内核线程由操作系统内核支持。

2.并发与并行

并发:一个时间段内有很多的线程或进程在执行,但何时间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行。
并行:一个时间段和时间点上都有多个线程或进程在执行。

3.多线程模型

多线程模型即用户级线程和内核级线程的不同连接方式,线程模型影响着并发规模及操作成本(开销)。

三种多线程模型:

1.使用用户线程实现(多对一模型 M:1)

多个用户线程映射到一个内核线程,用户线程建立在用户空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完成,对内核透明。

优点:

1) 线程的上下文切换都发生在用户空间,避免了模态切换(mode switch),减少了性能的开销。
2) 用户线程的创建不受内核资源的限制,可以支持更大规模的线程数量。

缺点:

1) 所有的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器可以被利用,浪费了其它处理器资源,不支持并行,在多处理器环境下这是不能够被接受的,如果线程因为 I/O 操作陷入了内核态,内核态线程阻塞等待 I/O 数据,则所有的线程都将会被阻塞。

2) 增加了复杂度,所有的线程操作都需要用户程序自己处理,而且在用户空间要想自己实现 “阻塞的时候把线程映射到其他处理器上” 异常困难。

2.使用内核线程实现(一对一模型 1:1)

程序使用的是内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程,轻量级进程也是属于用户线程。

所以这里统一称为用户线程。

每个用户线程都映射到一个内核线程,每个线程都成为一个独立的调度单元,由内核调度器独立调度,一个线程的阻塞不会影响到其他线程,从而保障整个进程继续工作。

优点:

1) 每个线程都成为一个独立的调度单元,使用内核提供的线程调度功能及处理器映射,可以完成线程的切换,并将线程的任务映射到其他处理器上,充分利用多核处理器的优势,实现真正的并行。

缺点:

1) 每创建一个用户级线程都需要创建一个内核级线程与其对应,因此需要消耗一定的内核资源,而内核资源是有限的,所以能创建的线程数量也是有限的。
2) 模态切换频繁,各种线程操作,如创建、析构及同步,都需要进行系统调用,需要频繁的在用户态和内核态之间切换,开销大。

3.使用用户线程加轻量级进程混合实现(多对多模型 M:N)

内核线程和用户线程的数量比为 M : N,这种模型需要内核线程调度器和用户空间线程调度器相互操作,本质上是多个线程被映射到了多个内核线程。

综合了前面两种模型的优点:

1) 用户线程的创建、切换、析构及同步依然发生在用户空间,能创建数量更多的线程,支持更大规模的并发。

2) 大部分的线程上下文切换都发生在用户空间,减少了模态切换带来的开销。

3) 可以使用内核提供的线程调度功能及处理器映射,充分利用多核处理器的优势,实现真正的并行,并降低了整个进程被完全阻塞的风险。

五、Java的线程模型

一句话总结:Java 的线程是映射到操作系统的原生线程之上的。

JVM 没有限定 Java 线程需要使用哪种线程模型来实现, JVM 只是封装了底层操作系统的差异,而不同的操作系统可能使用不同的线程模型,例如 Linux 和 windows 可能使用了一对一模型,solaris 和 unix 某些版本可能使用多对多模型。所以一谈到 Java 语言的多线程模型,需要针对具体 JVM 实现。

比如 Sun JDK 1.2开始,线程模型都是基于操作系统原生线程模型来实现,它的 Window 版和 Linux 版都是使用系统的 1:1 的线程模型实现的。

六、Java的并发为什么不如GO

以后再展开补充。

七、参考文献

《深入理解Java虚拟机》 – 周志明 第十二章第四小节
Go语言并发机制初探
关于Golang和JVM中并发模型实现的探讨