1、线程的实现

线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件 I/O 等),又可以独立调度。线程是 Java 里面进行处理器资源调度的最基本单位。

主流的操作系统都提供了线程实现,Java 语言提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行 start() 且还未结束的 java.lang.Thread 类的实例就代表了一个线程。我们注意到 Thread 类与大部分的 Java API 显著的区别,它的所有关键方法都是声明为 native 的。在 Java API 中,一个 native 方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现。实现线程主要有三种方式

  • 使用内核线程实现 1:1 实现
  • 使用用户线程实现 1:N 实现
  • 使用用户线程加轻量级进程或者实现 N:M 实现

1.1、内核线程实现

实现内核线程实现的方式也被成为 1:1 实现。内核线程(Kernel-Level Thread,KLT)就是直接有操作系统内核(Kernel)支持的线程,这种线程由内核来完成线程切换,内核调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就成为多线程内核(Multi-Threads Kernel)。

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

三、Java 与线程 - 图1

由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使其中某一个轻量级进程在系统调用中被阻塞了,也不会影响整个进程继续工作。轻量级进程也具有它的局限性

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

1.2、用户线程实现

使用用户线程实现的方式被称为 1:N 实现。广义上来讲,一个线程只要不是内核线程,都可以认为是用户线程(User Thread,UT)的一种。因此,从这个定义上看,轻量级进程也属于用户线程,但轻量级进程的实现始终是建立在内核之上的,许多操作都要进行系统调用,因此效率会受到限制,并不具备通常意义上的用户线程的优点。

而狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知到用户线程的存在及如何实现的。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也能够支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间的关系是 1:N 的关系称为一对多的线程模型,如图所示

三、Java 与线程 - 图2

用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要由用户程序自己去处理。线程的创建、销毁、切换和调度都是用户必须考虑的问题,而且由于操作系统只把处理器资源给配到线程,而且由于操作系统只把处理器资源分配到进程。例如,阻塞如何处理、多处理器系统中如何将线程映射到其他处理器上这类为题解决起来将会异常困难,甚至有些是不能实现的。因为使用用户线程的程序通常是比较复杂,除了有明确的需求外(譬如以前在不支持多线程的操作系统中的多线程程序、需要支持大规模线程的应用),一般的应用程序都不倾向使用用户线程。Java、Ruby 等语言都曾经使用过用户线程,最终又都放弃了使用它。但是近年来许多新的、以高并发为卖点的编程语言又普遍支持了用户线程,譬如 Golang、Erlang 等,使得用户线程的使用率有所回升。

1.3、混合实现

线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式,被称为 N:M 实现。在这种混合是线下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能以及轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程和轻量级进程的数量是不一定的,是 N:M 的关系,如图所示,这种就是多对多的线程模型。

三、Java 与线程 - 图3

许多 UNIX 系列的操作系统,如 Solaris、HP-UX 等都提供了 M:N 的线程模型实现。在这写操作系统上的应用也相对更容易应用 M:N 的线程模型。

1.4、Java 线程的实现

Java 线程如何实现并不受 Java 虚拟机规范的约束,这是一个与具体虚拟机相关的话题。Java 线程在早期的 Classic 虚拟机上(JDK1.2 以前),是基于一种被称为绿色线程(Green Threads)的用户线程实现的,但从 JDK1.3 起,主流平台上的主流商用 java 虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用 1:1 的线程模型。

以 HotSpot 为例,它的每一个 Java 线程都是直接映射到一个操作系统原生线程来实现的,而且中间没有额外的间接结构,所以 HotSpot 自己是不会去干涉线程调度的(可以设置线程优先级给操作系统提供调度建议),全权交给低下的操作系统去处理,所以何时冻结或唤醒线程、该给线程分配多少处理器执行时间、该把线程安排哪个处理器核心去执行等,都是由操作系统完成的,也都是由操作系统全权决定的。

前面强调是两个主流,那就是说明肯定有例外的情况,这里举两个著名的例子,一个是用于 Java ME 的 CLDC HotSpot Implementation(CLDC-HI)。它同时支持两种线程模型,默认使用 1:N 由用户线程实现的线程模型,所有 Java 线程都映射到一个内核线程上;不过它也可以使用另一种特殊的混合模型,Java 线程仍然全部映射到一个内核线程上,但当 Java 线程要执行一个阻塞调用时,CLDC-HI 会为该调用单独开一个内核线程,并且调度执行其他 Java 线程,等到那个阻塞调用完成之后再重新调度之前的 Java 线程继续执行。

另一个例子是在 Solaris 平台上的 HotSpot 虚拟机,由于操作系统的线程特性本来就可以同时支持 1:1(通过 Bound Threads 或 Alternate Libthread 实现) 及 N:M(LWP/Thread Based Synchroniztion 实现) 的线程模型,因此 Solaris 版的 HotSpot 也对应提供了两个平台专有的虚拟机参数,即 -XX:+UseLWPSynchronization(默认值)和 -XX:+UseBoundThreads 来明确指定虚拟机使用哪种线程模型。

1.5、Java 程序

Java 程序天生就是多线程程序,下面使用 JMX 来查看一个普通的 Java 程序包含哪些线程,代码如下:

  1. package com.yj.thread;
  2. import java.lang.management.ManagementFactory;
  3. import java.lang.management.ThreadInfo;
  4. import java.lang.management.ThreadMXBean;
  5. /**
  6. * @description: Java 线程
  7. * @author: erlang
  8. * @since: 2021-02-01 19:59
  9. */
  10. public class MultiThread {
  11. public static void main(String[] args) {
  12. // 获取 Java 线程管理 MXBean
  13. ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
  14. // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
  15. ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
  16. for (ThreadInfo threadInfo : threadInfos) {
  17. System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
  18. }
  19. }
  20. }

结果如下,可以看出一个 Java 程序的运行不仅仅是 main 方法的运行,而是 main 线程和多个其他线程的同时运行。

  1. [5] Monitor Ctrl-Break // idea 特有的线程
  2. [4] Signal Dispatcher // 分发处理器发送诶 JVM 信号的线程
  3. [3] Finalizer // 调用对象 finalize 方法的线程
  4. [2] Reference Handler // 清除 Reference 的线程
  5. [1] main // main 线程,用户线程入口

2、Java 线程调度

线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Scheduling)。

2.1、协同式线程调度

使用协同式线程调度的多线程系统,线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个系统上去。协同式多线程调度的优缺点:

  • 优点:
    实现简单,由线程把自己的事情干完之后才会进行线程切换,切换操作对线程自己是可知的,所以一般没有什么线程同步的问题
  • 缺点:
    线程执行时间不可控制,甚至如果一个线程的代码编写有问题,一直不告知系统线程切换那么程序就会一直阻塞在哪里

2.2、抢占式线程调度

使用抢占式线程调度的多线程系统,那么每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。例如在 Java 中,Thread::yield 方法可以主动让出执行时间,但是如果想要主动获取执行时间,线程本身是没有什么办法的。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会出现一个线程导致整个进程甚至整个系统阻塞问题。当一个进程出现了问题,我们还可以使用任务管理器把这个进程杀掉,而不至于导致系统崩溃。

2.3、Java 的线程调度方式

Java 使用的线程调度方式就是抢占式调度。我们可以通过设置线程优先级来实现建议操作系统给某些线程多分配一点执行时间,另外的一些线程则可以少分配一点。Java 语言一共设置了 10 个级别的线程优先级(1~10)。在两个线程同时处于 Ready 状态时,优先级越高的线程越容易被系统选择执行。

不过,线程优先级并不是一项稳定的调解手段,主流虚拟机上的 Java 线程是被映射到系统的原生线程上来实现的,所以线程调度最终还是由操作系统说了算。尽管现代的操作系统基本都提供喜爱昵称优先级的概念,但是并不保证能与 Java 线程的优先级一一对应,如 Solaris 中线程有2147483684(2 的 31 次幂)种优先级,Windows 中只有七种优先级。

如果操作系统的优先级比 Java 线程优先级更多,中间留出一点空位;反之,就需要几个线程优先级对应到同一个操作系统优先级。如表所示,Java 线程优先级和 Windows 线程优先级之间的对应关系。

Java 线程优先级 Windows 线程优先级
1 Thread.MIN_PRIORITY THREAD_PRIORITY_LOWEST
2 THREAD_PRIORITY_LOWEST
3 THREAD_PRIORITY_BELOW_NORMAL
4 THREAD_PRIORITY_BELOW_NORMAL
5 Thread.NORM_PRIORITY THREAD_PRIORITY_NORMAL
6 THREAD_PRIORITY_ABOVE_NORMAL
7 THREAD_PRIORITY_ABOVE_NORMAL
8 THREAD_PRIORITY_HIGHEST
9 THREAD_PRIORITY_HIGHEST
10 Thread.MAX_PRIORITY THREAD_PRIORITY_CRITICAL

线程优先级并不是一项稳定的调解手段,这不仅仅体现在某些操作系统上不同的优先级实际会变得相同;还有其他情况让我不能过于依赖线程优先级。优先级可能会被系统自行改变, 例如在 Windows 系统中存在一个叫优先级推进器的功能,大致作用是当系统发现一个线程被执行得特别频繁时,可能会越过线程优先级去为它分配执行时间,从而减少因为线程频繁切换而带来的性能损耗。因此,我们并不能在程序中通过优先级来完全准确判断一组状态都为 Ready 的线程将会先执行哪一个。优先级示例代码如下:

  1. package com.yj.thread;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * @description: 线程优先级测试
  7. * @author: erlang
  8. * @since: 2021-02-01 20:42
  9. */
  10. public class ThreadPriority {
  11. static volatile boolean notStart = true;
  12. static volatile boolean notEnd = true;
  13. public static void main(String[] args) throws InterruptedException {
  14. List<PriorityRunnable> runnableList = new ArrayList<>();
  15. for (int i = 1; i < 11; i++) {
  16. PriorityRunnable runnable = new PriorityRunnable(i);
  17. runnableList.add(runnable);
  18. Thread thread = new Thread(runnable, "Thread: " + i);
  19. thread.setPriority(i);
  20. thread.start();
  21. }
  22. notStart = false;
  23. TimeUnit.SECONDS.sleep(10);
  24. notEnd = false;
  25. for (PriorityRunnable runnable : runnableList) {
  26. System.out.println("runnable priority: " + runnable.priority + ", count: " + runnable.count);
  27. }
  28. }
  29. static class PriorityRunnable implements Runnable {
  30. private int priority;
  31. private long count;
  32. public PriorityRunnable(int priority) {
  33. this.priority = priority;
  34. }
  35. @Override
  36. public void run() {
  37. while (notStart) {
  38. Thread.yield();
  39. }
  40. while (notEnd) {
  41. Thread.yield();
  42. count++;
  43. }
  44. }
  45. }
  46. }

实例代码的运行结果如下,从结果可以看到线程优先级并没有生效,程序的正确性不能依赖线程的优先级高低。

  1. runnable priority: 1, count: 3007262
  2. runnable priority: 2, count: 2972542
  3. runnable priority: 3, count: 2961771
  4. runnable priority: 4, count: 2968190
  5. runnable priority: 5, count: 2960007
  6. runnable priority: 6, count: 3147635
  7. runnable priority: 7, count: 3034409
  8. runnable priority: 8, count: 2965675
  9. runnable priority: 9, count: 2959709
  10. runnable priority: 10, count: 2952653