Java线程

线程的实现

  实现线程主要有 3 种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。

1.使用内核线程实现

  内核线程(Kernel-Level Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多个任务,支持多线程的内核叫做多线程内核

  程序一般不会直接去使用内核线程,而是使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),也就是我们通常意义上所讲的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程和内核线程之间 1 : 1 的关系称为一对一的线程模型,如下图。

image.png

  由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态和内核态中来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此,一个系统支持的轻量级进程的数量是有限的。

  Sun JDK 的 windows 版与 Linux 版都是使用此模型实现的。

2.使用用户线程实现

  广义上讲,一个线程只要不是内核线程,就可以认为是用户线程(User Thread,UT),那轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,效率会受到限制。

  狭义上的用户线程是指完全建立在用户控件的线程库上,系统内核不能感知线程存在,用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。这种线程是不需要切换到内核态,因此操作时非常快速且低消耗的。这种实现可以支持规模更大的线程数量,这种进程与用户线程之间 1 : N 的关系称为一对多的线程模型。

image.png

  使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理,创建、切换、调度、销毁都非常复杂。现在使用蔗渣模型的程序越来越少了,Ruby、Java曾经使用过用户线程,最终又放弃了它。

3.使用用户线程加轻量级进程混合实现

  许多Unix操作系统,如 Solaris、HO-UX 提供了 N : M 的线程模型实现,这种模型其实就是将上面两种方式混合,如下图:
image.png

Java线程调度

  线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种:协同式线程调度抢占式线程调度。

  协同式调度的多线程系统中,线程的执行时间是由线程本身来控制的,线程把自己的工作执行完了后,要主动通知系统切换到另外一个线程上。这种实现的最大好处是实现简单,切换操作对于线程自己事可知的,所以没有线程同步的问题。坏处也很明显:线程执行时间不可控,甚至如果一个线程编写有问题,一直不告诉系统进行线程切换,那程序就会阻塞,甚至系统崩溃。

  如果使用抢占式调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。Java使用的就是抢占式调度

操作系统中的线(进)程状态转换

  首先要简单说下操作系统中线程的状态,其有五个状态,除了创建和终止,还有就绪、运行和阻塞状态,当发生 I/O 或其它事件时,会从运行状态转变为阻塞状态,当 I/O 结束时,便从阻塞状态转变为就绪状态,此时只要有 CPU 时间片就可以立即变为运行状态。
进程转换状态

不难发现操作系统中的线程状态都是围绕着 CPU 来说的

Java线程状态转换

  Java线程定义了 6 种线程状态:

  • 新建(New):创建后尚未启动的线程处于这种状态。
  • 运行(Runbale):包括了操作系统线程状态中的 Running 和 Ready,也就是处于此状态的线程可能正在执行,也可能正在等待 CPU 为它分配执行时间。
  • 无限期等待(Waiting):处于这种状态的线程不会被分配 CPU 时间,它们要等待被其它线程显示地唤醒,以下方法会让线程陷入此状态:
    • 没有设置 timeout 参数的 Object.wait() 方法。
    • 没有设置 timeout 参数的 Thread.join() 方法。
    • LockSupport.park() 方法。
  • 期限等待(Timed Waiting):处于这种状态的线程不会被分配 CPU 时间,不过无须等待被其它线程显示地唤醒,在一定时间后它们会由系统自动唤醒,以下方法会让线程陷入此状态:
    • Thread.sleep() 方法。
    • 设置了 timeout 参数的 Object.wait() 方法。
    • 设置了 timeout 参数的 Thread.join() 方法。
    • LockSupport.parkNanos() 方法。
    • LockSupport.parkUntil() 方法。
  • 阻塞(Blocked):线程被阻塞了,阻塞与等待的区别是阻塞是在等待着获取到一个排它锁,这个事件将在另外一个事件放弃这个锁的时候发生,而等待状态则是在等待一段时间或唤醒动作的发送。当程序进入同步区域的时候,线程将进入阻塞状态。
  • 结束(Terminated):线程已经结束执行。

  需要注意的是,Java中的线程状态并不是与操作系统中的线程状态一一对应的。查看 Thread 类的源代码,可以看见 RUNNABLE 状态的注释中说了处于这个状态线程可能正在 JVM 中执行,也可能正在等待操作系统的某种资源。所以,RUNNABLE 状态至少包括了操作系统中的就绪和运行状态的。

1
2
3
4
5
6
7
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE

  当发生 I/O 阻塞时,操作系统概念下的线程会切换到阻塞状态,但 Java 的线程状态也有阻塞,还有无期限等待和期限等待,那此时 Java 的线程状态是什么呢?其实也还是 RUNNABLE,可以写一个简单 demo 来验证:

image.png

  同理,网络阻塞时,Java的线程状态也还是 RUNNABLE。Java 中的 RUNNABLE 实际对应了操作系统中的 就绪状态、运行状态和部分阻塞状态。看起来很混乱,那 Java 为什么要这么做呢?前面 RUNNABLE 的注释说到该状态的线程可能正在JVM中执行,也可能正在等操作系统的资源,JVM 把很多东西都看作了资源,硬盘、CPU、网卡也罢,只要有一个东西在为线程服务,那就可以理解为这个线程是在“执行”的,虽然 I/O 阻塞, CPU 不执行了,但网卡还在监听干活啊,只是暂时没有数据罢了。操作系统的线程状态都是围绕着 CPU 来说的,这与 JVM 的侧重点不同。

  如果 Java 中的线程处于期限等待、无期限等待或阻塞状态,那说明这个线程真的是一点事都不干了,而且是你自己不让它干的,例如调用了 sleep()、wait() 方法。

参考:https://my.oschina.net/goldenshaw/blog/705397