概述
前面几篇博客我系统的整理了 java 线程类 Thread 的使用、线程状态及并发安全性问题。本篇博客我打算简单的介绍一下 java 线程和操作系统线程之间的关联,以及 java 线程如何被调度。
java 线程模型
本篇博客我打算分以下几个模块展开:
- 线程和进程
- 线程模型
- java 线程模型及调度方式
1、进程和线程
线程和进程的概念常常在操作系统中被提到,一般它们都是这样被定义的:
- 进程是操作系统分配资源的基本单位
- 线程是 CPU 调度的基本单位
早期的操作系统中只有进程的概念,CPU 通过直接调用进程完成任务。任务的并发执行通过多进程实现。后来随着计算机应用的越来越庞大,进程所占据的资源也越来越多。CPU 为了实现任务的并发执行,切换进程所带来的的损耗也越来越大。为了解决上述问题,操作系统引入了更轻量级的线程。
线程是比进程更轻量级的调度单位。通过线程,可以把进程中的资源分配和资源调度分开,所有线程共享进程资源,又独立调度。关于线程和进程的关系我列出以下几条:
- 线程在进程下运行
- 一个进程可以包含多个线程,至少包含一个线程
- 不同进程间数据不共享,不同线程间数据也不共享
- 同一进程下所有线程共享进程数据
- 调度进程会带来更多的计算机资源消耗
- 进程间不会互相影响,进程中线程挂掉会导致整个进程挂掉
现如今主流的操作系统都实现了线程的概念,java 代码中通过 Thread.start() 启动一个线程。该方法底层也是通过调用 JNI 方法实现的,也就是说 java 创建线程也是根据底层平台实现的。
2、线程模型
常见的线程模型有以下三种:
- 内核线程模型
- 用户线程模型
- 混合线程模型
2-1、内核线程模型
内核线程模型即完全依赖操作系统内核线程来完成多线程并发。在此模型下,线程的调度通过内核来完成。系统内核通过将多个线程执行的任务映射到CPU上完成。
一般情况下应用程序不会直接调用内核线程, 而是采用内核线程向上提供的接口 轻量级进程。我们可以抽象的把轻量级进程理解为线程,每个轻量级进程都会有一个内核线程与之一一对应。我们把这种轻量级进程比内核线程等于1:1的模型也称为 一对一线程模型。
下面我通过简单抽象绘图描述一下这种模型:
- LWP:Light Weight Process,即轻量级进程
- KIT:Kernel-Level Thread,即内核线程
- Thread Scheduler:线程调度器,它是一个操作系统服务,为正在运行的内核线程分配 CPU 时间。
在这种模式下,用户进程中某个线程(轻量级进程)的阻塞不会导致整个进程的阻塞,其他未阻塞的线程还可以被 CPU 调度,整个进程还可以正常工作。但内核线程也存在以下缺陷:
- 由于线程(轻量级进程)本质还是基于内核线程实现,因此各种线程的创建、同步等操作都需要采用系统调用,而系统调用本身代价比较大,需要线程在用户态和内核态之间不断切换。
- 由于每个线程(轻量级进程)都需要一个内核线程相对应,因此线程的创建需要耗费一定的内核资源,而内核资源本身较少,也就是说系统所能支持的线程数是有限的
2-2、用户线程模型(存疑)
在内核线程模型中,我们提到了用户态和内核态,这里我简单提一下这两种状态的概念:
- 用户态:处于用户态的线程只能访问上层应用资源和代码
- 内核态:处于内核态的线程可以访问内核中的资源和代码,还可以执行操作系统层面的函数
简单来说,划分用户态和内核态主要是为了防止任何线程都可以操作内核资源,导致整个系统崩溃。关于用户态和内核态的介绍先提到这里,后面我们专门出博客介绍。
根据用户态和内核态的概念,线程也可以被划分为 用户线程 和 内核线程。上文我们提到的轻量级进程从宏观角度看也是一种用户线程。但是轻量级进程本质还是基于内核线程实现,它的很多操作需要切换到内核态执行,因此它可以被看做是一种特殊的用户线程。
从微观角度看,用户线程是指完全建立在用户态的线程,它的创建、启动、阻塞,停止等操作可以在用户态独立完成,不需要内核线程的帮助。如果处理的好的话,用户线程永远不需要切换为内核线程。由于省去了线程状态切换所带来的消耗,这种用户线程是非常高效的。又因为线程在用户态被创建,用户态相比内核态所占内存更大,因此可以支持更多的线程并发数量。这种进程比线程等于1:N的线程模型我们也称为 一对多线程模型。
下面我通过简单抽象绘图描述这种线程模型:
- UT:用户线程,微观角度上的用户线程
用户线程模型的优势上面已经做过介绍,下面我们主要来看用户线程模式的缺陷:
因为没有内核线程的原因,所有线程操作都需要用户程序自己解决,像线程的创建、运行、阻塞、切换等都需要用户自己实现。CPU 调度到进程后,进程内部线程的运行法则需要应用程序自己来维护。一般情况下,想要实现一套线程运行法则是非常困难的。因此大多数采用用户线程模型的应用直接调用第三方线程框架来完成操作。
存疑:在整理这部分博客时,我一直在考虑用户线程模型是否真的可以完全不借助内核线程,一个不借助内核线程的应用如何访问内核资源,如果不能访问内核资源的话,它又怎么保证任务能顺利完成。
关于这块可能还有一种新猜想:这里的一对多模型是指用户进程中的所有线程对应某一个内核线程,而不是说几乎不使用内核线程。再查阅很多博客及书籍后,关于这块内容我越来越迷糊。暂时先标记存疑,后续如果有机会解决的话再修改。
2-3、混合线程模型
除了完全使用用户线程或完全使用内核线程的实现方式外,还有一种将两者混合的实现方式。在这种混合线程模型下,既有用户线程,也有内核线程和轻量级进程。
下面我通过简单抽象绘图描述一下这种模型:
在混合线程模型中,用户线程的操作都是建立在用户空间中。因此线程的创建、切换,回收等操作不需要切换到内核态执行,并且支持大规模的线程并发执行。
混合线程模型中的轻量级进程作为媒介连接用户线程和内核线程,这样可以使用内核提供的线程调度功能及处理器映射,此时用户线程的系统调用通过内核线程来完成。
在混合线程模型中,一个 LWP 可以对接多个 UT,而每个用户进程又包含多个 LWP。这种用户线程比轻量级进程为 N:M 的线程模型也被称为 混合线程模型。
3、java 线程模型及调度方式
在 Windows 和 linux 操作系统中,绝大多数 jvm 都采用一对一线程模型,即一个 java 线程对应一个内核线程,线程的创建、阻塞、回收等操作都通过调用 JNI 方法在底层平台实现。
有了 java 线程的模型,我们再来看看 java 线程调度的规则。常见的调度规则有以下两种:
-
协同式调度:线程执行完毕后通知操作系统切换到其他线程。也就是说线程本身可以确定自己所执行的时间,并且主动通知操作系统来结束执行。它最大的优点就是简单,并且由于线程可控制,因此一般情况下不需要考虑同步问题。它的缺点也非常明显:如果一个线程由于异常无法执行完毕,那么它将永远不会释放CPU资源,其他线程也无法被调度。
-
抢占式调度:线程的执行时间由操作系统统一分配。也就是说线程本身不能确定自己所执行的时间,时间片执行完或线程阻塞都会导致切换到其他线程。它最大优点就是即使线程存在异常,也不会导致该线程一直占有CPU,以致于其他线程无法正常运转。它的缺点也非常明显:并发执行锁带来的线程安全性问题。
java 使用的线程调度方式是抢占式调度。为了尽可能增加对线程的控制,java 线程还引入了 yield() 方法和 线程优先级 的概念。
-
当线程调用 yield() 方法后,当前线程会释放CPU资源,重新等待 CPU 调度。值得一提的是,执行 yield() 方法释放 CPU 资源的线程也可能立即获取到 CPU 资源继续向下执行。
-
线程优先级是一个比较模糊的概念,并不能起到决定性作用。也就是说,优先级高的线程不一定先执行,优先级低的线程同样也不一定后执行。线程优先级只是尽可能的让优先级高的线程先执行。因为 java 线程的一对一模型决定它是基于内核线程实现的,内核线程的调度次序由底层平台决定,并不能完全契合java 优先级。
关于 java 线程状态等概念已经通过其他博客详细介绍,这里不做过多赘述。