进程与线程
在传统的操作系统中,最核心的概念是“进程”,进程是对正在运行的程序的一个抽象。
进程的存在让“并行”成为了可能,在一个操作系统中,允许运行着多个进程,这些进程“看起来”是同时在运行的。
如果我们的计算机同时运行着 web 浏览器、电子邮件客户端、即时通讯软件例如QQ微信等多个进程,我们感觉这些进程都是同时在运行的,假设这台计算机搭配的是多个 CPU 或者 多核 CPU,那么这种多个进程并行的现象可能一点也不奇怪,完全可以为每个进程单独分配一个 CPU,这样就实现了多进程并行。
然而事实上,在计算机只有一个 CPU 的情况下,它也能给人类一种感觉:多个进程同时在运行。但人类的感觉往往是比较模糊的,不精确的。事实是由于 CPU 的计算速度非常地快,它能快速地在各个进程之间切换,在某一瞬间,CPU 只能运行一个进程,但一秒钟之内,它就能通过快速切换,让人产生多个进程同时在运行的错觉。
在操作系统中,为什么在进程的基础上,又衍生出了线程的概念呢?
- 由于对于一些进程而言,它内部会发生多种活动,有些活动可能会在某个时间里阻塞,有些活动不会,如果通过线程将这些活动分离开使它们能够并行地运行,则设计程序的时候会更加简单。
- 线程比进程的创建更加轻量级,性能消耗更少
- 如果一个进程既需要 CPU 计算,也需要I/O处理,拥有多线程允许这些活动重叠进行,加快整个进程的执行速度。
每一个进程在操作系统中都拥有独立的一块内存地址空间,该进程创建的所有线程共享这块内存,支持多线程的操作系统,会让线程作为 CPU 调度的最小单位。CPU 的时间片在不同的线程之间进行分配。
线程的可能实现方式
基本上主流的操作系统都支持线程,也提供了线程的实现。而 Java 语言为了应对不同硬件和操作系统的差异,提供了对线程操作的统一抽象,在 Java 中我们使用 Thread 类来代表一个线程。
Thread 的具体实现可能会有不同的实现方式:
使用内核线程实现
内核线程是操作系统内核支持的线程,在内核中有一个线程表用来记录系统中的所有线程,创建或者销毁一个线程时,都需要涉及到系统调用,然后再内核中对线程表进行更新操作。对内核线程的阻塞以及其它操作,都涉及到系统调用,系统调用的代价都比较大,涉及到在用户态和内核态之间的来回切换。此外,内核内部有线程调度器,用于决定应该将 CPU 时间片分配个哪个线程。
程序一般不会直接操作内核线程,而是使用内核线程的一种高级接口,轻量级进程。轻量级进程与内核线程之间的关系是 1:1,每一个轻量级进程内部都有一个内核线程支持。
上图中, LWP 指 Light Weight Process,即轻量级进程;KLT 指 Kernel Level Thread,即内核线程。
使用用户线程实现
用户线程是程序或者编程语言自己实现的线程库,系统内核无法感知到这些线程的存在。用户线程的建立、同步、销毁和调度,都在用户态中完成,无须内核的帮助,不需要进行系统调用,这样的好处是对于线程的操作是非常高效的。在这种情况下,进程和用户线程的比例是 1 :N。
用户态线程面对如何阻塞线程时,会面临困难,阻塞一个用户态线程会出现把整个进程都阻塞的情况,多线程也就失去了意义。因为缺少内核的支持,所以很多需要利用内核才能完成的工作,例如阻塞与唤醒线程、多 CPU 环境下线程的映射等,都需要用户程序去实现,实现起来会异常困难。
使用用户线程和内核线程混合实现
在这种混合实现下,既存在用户线程,也存在内核线程。用户态线程的创建、切换这些操作依然很高效,并且用户态实现的线程,比较容易加大线程的规模。需要操作系统内核支持的功能,则通过内核线程来做到,例如映射到不同的处理器上、处理线程的阻塞与唤醒以及内核线程的调度等。这种实现依然会使用到轻量级进程 LWP,它是用户线程和内核线程之间的桥梁。
Java 线程的实现
在 JDK1.2 之前, Java 的线程是使用用户线程实现的,在 JDK1.2 之后,Java 才采用操作系统原生支持的线程模型来实现,Java 线程模型的实现方式,主要取决于操作系统。对于 Oracle JDK 来说,在 Windows 和 Linux 上的线程模型是采用一对一的方式实现的,即一条 Java 线程映射到一条轻量级进程(内核线程)。而在 Solaris 平台中,Java 则支持 1 :1 和 N : M 的线程模型。
线程的阻塞与等待
Java 中的线程的状态有以下几种:New,Runnable,Waiting, TimedWaiting, Blocked,Terminated。
- NEW:线程初创建,未运行
- RUNNABLE:线程正在运行,但不一定消耗 CPU
- BLOCKED:线程正在等待另外一个线程释放锁
- WAITING:线程执行了 wait, join, LockSupport.park() 方法
- TIMED_WAITING:线程调用了sleep, wait, join, LockSupport.parkNanos() 等方法,与 WAITING 状态不同的是,这些方法带有表示时间的参数。
其中 Blocked 和 Waiting 有个重要的区别是,阻塞(Blocked)状态的线程在等待获取一个排他锁,例如线程在等待进入一个synchronized关键字包围的临界区时,就进入 Blocked 状态。而 Waiting 状态则是在等待被唤醒,或者等待一段时间。
参考资料
《深入理解 Java 虚拟机》第二版 - 周志明
《现代操作系统》第四版 - Andrew S. Tanenbaum