【Java并发编程】Java线程模型&线程调度原则

1 Java线程模型

Java语言的线程,从规范的角度来说是不强制要求任何具体的实现方式的。采用1:1、N:1、M:N模型都可以。先放个传送门:RednaxelaFX:JVM中的线程模型是用户级的么

N : 1(JDK2前)

Java线程在JDK1.2之前,是基于称为“绿色线程”(Green Threads)的用户线程实现的,而在JDK1.2中,线程模型替换为基于操作系统原生线程模型来实现。因此,在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上决定了Java虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定Java线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对Java程序的编码和运行过程来说,这些差异都是透明的。

也就说JDK1.2之前,程序员们为JVM开发了自己的一个线程调度内核,而到操作系统层面就是用户空间内的线程实现。而到了JDK1.2及以后,JVM选择了更加稳健且方便使用的操作系统原生的线程模型,通过系统调用,将程序的线程交给了操作系统内核进行调度

对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的,一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的。

也就是说,现在的Java中线程的本质,其实就是操作系统中的线程,Linux下是基于pthread库实现的轻量级进程,Windows下是原生的系统Win32 API提供系统调用从而实现多线程

1 : 1(HotSpot)

具体到我们平时常用的JVM实现,Oracle/Sun的HotSpot VM,它是用1:1模型来实现Java线程的,也就是说一个Java线程是直接通过一个OS线程来实现的,中间并没有额外的间接结构。而且HotSpot VM自己也不干涉线程的调度,全权交给底下的OS去处理。所以如果OS想把某个线程调度到某个CPU/核上,它就自己弄了。

这个意义上说Java程序跑在HotSpot VM上开多个Java线程,就跟一个C/C++程序开了多线程来跑没有任何两样。那么怎么控制这些线程分布到不同的CPU核上去呢?

在Linux上的话,可以用taskset来把线程绑在某个指定的核上。在Java层面上,有大佬写了个现成的库来利用taskset绑核:OpenHFT/Java-Thread-Affinity 有兴趣的话可以参考一下。

最后提一下:JVM的实现有很多种,并不是所有JVM都像HotSpot VM这样总是用1:1模型的。前面的传送门已经有例子了所以这里就不多说,但还是想特别提一下免得给初学者留下“Java线程就肯定是OS线程”的误解。请务必针对实现来讨论这种问题。

2.Java线程调度

2.1 JVM调度(JDK2前)

在早期的java1.1中,JVM自己实现线程调度,而不依赖于底层的平台。绿色线程(用户级线程)是JVM使用的唯一的线程模型(至少是在solaris平台上)。

(1)JVM使用抢占的、基于优先权的调度策略;

(2)每个线程都有优先级,JVM总是选择最高优先级的线程执行;

(3)若果两个线程具有相同的优先级,则采用FIFO的调度顺序。
  • 绿色线程的执行时间由线程本身来控制,线程自身工作告一段落后,要主动告知系统切换到另一个线程上。其特点是实现简单,不需要专门的硬件支持,切换操作对线程自身来说是预先可知的。
  • 因为绿色线程库是用户级的,并且Solaris一次只能处理一个绿色线程,即Java运行时采用多对一的线程模型,所以会导致如下问题:
    • Java应用程序不能与Solaris环境中的多线程技术互操作,就是说Solaris管理不了Java线程;
    • Java线程不能在多处理机上并行执行
    • Java 应用不能享用操作系统提供的并发性
      由于绿色线程的这些限制,在java1.2之后的版本中放弃了该模型,而采用本地线程(Native threads,是指使用操作系统本地的线程库建立和管理的线程),即将Java线程连接到本地线程上,主要由底层平台实现线程的调度

2.2 底层平台调度

Java语言规范和Java虚拟机规范是Java的重要文档,可惜的是他们都没有说明Java线程的调度问题。或许从Java的角度看,线程并不是Java最基本的内容。毕竟Thread类也仅仅是Java一个特定的类而已。

终于在Java SE 8 API规范的Thread类说明中算是找到了线程调度的有关描述:

  • 每个线程有一个优先级(从1级到10级),较高优先级的线程比低优先级线程先执行。
  • 程序员可以通过Thread.setPriority(int)设置线程的优先级,默认的优先级是NORM_PRIORITY。
  • Java SE 还声明JVM可以任何方式实现线程的优先级,甚至忽略它的存在。

我们是通过Java创建的线程,线程调度的事儿Java是脱不开的。那Java又是如何将线程调度交给底层的操作系统去做呢?下面我们将跟随JVM虚拟机底层平台上的实现,说明Java线程的调度策略。

既然Java底层的运行平台提供了强大的线程管理能力,Java就没有理由再自己进行线程的管理和调度了。于是JVM放弃了绿色线程的实现机制,将每个Java线程一对一映射到底层平台上的一个本地线程上,并将线程调度交由本地线程的调度程序。由于Java线程是与本地线程是一对一地绑在一起的,所以改变Java线程的优先权也不会有可靠地运行结果。

2.2.1 Solaris

Solaris早期版本还是尽量实现了基本的用户模式下的抢占

系统维护这一条戒律:就绪队列上任何线程的优先级必须小于等于正在运行的线程,否则,优先级最低的正在运行的线程将被剥夺运行的机会,即将其对应的LWP让给优先级高的本地线程。在如下三种情况下会发生线程的抢占:

  • 当正在运行的本地线程降低了其优先级,使其小于就绪队列中的某个线程的优先级
  • 当正在运行的本地线程增加了就绪队列中某个线程的优先级,使其高于正在运行的线程的优先级
  • 当就绪队列中新加入了一个优先级高于正在运行的线程的优先级,例如,某个高优先级的线程被唤醒。

Java线程的唤醒、优先级设置是由JVM实现的,但线程的调度(与LWP的连接)则是由本地线程库完成。操作系统(Solaris)可以依据自己的原则改变LWP的优先级,例如,通过动态优先级实现分时,这是线程库和JVM都无法干预的。

Solaris 9之后,使用了1:1的线程模型

即本地线程与LWP一对一地绑在一起,本地线程库也失去了直接干预线程调度的机会(指为本地线程选择连接LWP)。Java线程也就通过本地线程与LWP终生地一对一地绑在一起。这样可以通过改变本地线程或Java线程的优先级来影响LWP的优先级,从而影响系统的CPU调度。但具体的CPU分配策略还是Solaris做出的,JVM仅起辅助的作用。

2.2.2 Windows

在Windows下,Java线程一对一地绑定到Win32线程(相当于Solaris的native线程)上。当然Win32线程也是一对一地绑定到内核级线程上,所以Java线程的调度实际上是内核完成的。Java虚拟机可以做的是通过将Java线程的优先级映射到Win32线程的优先级上,从而影响系统的线程调度决策。

优先级划分

Windows内核使用了32级优先权模式来决定线程的调度顺序。优先权被分为两类:可变类优先权包含了1-15级,不可变类优先权(实时类)包含了16-31级。调度程序为每一个优先级建一个调度队列,从高优先级到低优先级队列逐个查找,直到找到一个可运行的线程

Win32将进程(process)分为如下6个优先级类:
REALTIME_PRIORITY_CLASS
HIGH_PRIORITY_CLASS
ABOVE_NORMAL_PRIORITY_CLASS
NORAL_PRIORITY_CLASS
BELOW_NORMAL_PRIORITY_CLASS
IDLE_PRIORITY_CLASS

为区分进程内线程的优先级,每个优先级类又包含6个相对优先级:
TIME_CRITIAL
HEGHEST
ABOVE_NORNAL
NORMAL
BELOW_NORMAL
LOWEST
IDLE

当把Java 线程绑定到Win32线程时,需要将Java线程的优先级映射到Win32线程上。Java 6在Windows的实现中将Java线程的优先级按下表所示映射到Win32线程的相对优先级上。

img

当JVM将线程的优先级映射到Win32线程的优先级上之后,线程调度的工作就是Win32和Windows内核的事儿了。

抢占式

Windows采用基于优先级的、抢占的线程调度算法。调度程序保证总是让具有最高优先级的线程运行。一个线程仅在如下四种情况下才会放弃CPU:

  • (1)被一个更高优先级的线程抢占;
  • (2)结束;
  • (3)时间片到;
  • (4)执行导致阻塞的系统调用。

请注意,尽管Windows采用了基于优先级的调度策略,但不会出现饥饿现象。其采取的主要措施是:优先级再高的的线程也会在运行一个时间片之后放弃CPU,并且降低其优先级,从而保证了低优先级线程也有机会运行。

动态优先级

  • 当线程的时间片用完后,降低其优先级;
  • 当线程从阻塞变为就绪时,增加线程的优先级;
  • 当线程很长时间没有机会运行时,系统也会提升线程的优先级。
  • Windows区分前台和后台进程,前台进程往往获得更长的时间片。

以上这些措施体现了Windows基于动态优先级、分时和抢占的CPU调度策略。调度策略很复杂,考虑了线程执行过程的各个方面,再加上系统运行环境的变化,我们很难通过线程运行过程的观察理清调度算法的全貌。

由于Java线程到Windows内核线程一对一的绑定方式,所以我们看到的Java线程的运行过程实际上反映的是Windows的调度策略

2.2.3 Linux

同Windows一样,在Linux上Java线程一对一地映射到内核级线程上。不过Linux中是不区分进程和线程的,同一个进程中的线程可以看作是共享程度较高的一组进程。Linux也是通过优先级来实现CPU分配的,应用程序可以通过调整nice值(谦让值)来设置进程的优先级。nice值反映了线程的谦让程度,该值越高说明这个线程越有意愿把CPU让给别的线程,nice的值可以由线程自己设定。所以JVM需要实现Java线程的优先级到nice的映射,即从区间[1,10]到[19, -20]的映射。把自己线程的nice值设置高了,说明你的人品很谦让,当然使用CPU的机会就会少一点。

linux调度器实现了一个抢占的、基于优先级的调度算法,支持两种类型的进程的调度:实时进程的优先级范围为[0,99],普通进程的优先级范围为[100,140]。
img
进程的优先权越高,所获得的时间片就越大。每个就绪进程都有一个时间片。内核将就绪进程分为活动的(active)和过期的(expired)两类:只要进程的时间片没有耗尽,就一直有资格运行,称为活动的;当进程的时间片耗尽后,就没有资格运行了,称为过期的。调度程序总是在活动的进程中选择优先级最高的进程执行,直到所有的活动进程都耗尽了他们的时间片。当所有的活动进程都变成过期的之后,调度程序再将所有过期的进程置为活动的,并为他们分配相应的时间片,重新进行新一轮的调度。所以Linux的线程调度也不会出现饥饿现象。

在Linux上,同Windows的情况类似,Java线程的调度最终转化为了操作系统中的进程调度。

2.3 总结

从以上Java在不同平台上的实现来看,只有在底层平台不支持线程时,JVM才会自己实现线程的管理和调度,此时Java线程以绿色线程的方式运行。由于目前流行的操作系统都支持线程,所以JVM就没必要管线程调度的事情了。应用程序通过setPriority()方法设置的线程优先级,将映射到内核级线程的优先级,影响内核的线程调度。

目前的Java的官方文档中几乎不再介绍有关Java线程的调度算法问题,因为这确实不是Java的事儿了。尽管程序中还可以调用setPriority(),提请JVM注意线程的优先级,但你千万不要把这事儿太当真。Java中所谓的线程调度仅是底层平台线程调度的一个影子而已。

由于Java是跨平台的,因此要求Java的程序设计不能对Java线程的调度方法有任何假设,即程序运行的正确性不能依赖于线程调度的方法。所以说程序员最好不要过分关心底层平台是如何实现线程调度的,只要知道他们是并发运行的就可以了,甚至不必在意线程的优先级,因为优先级也不靠谱。正如Joshua Bloch在他的书《Effective Java》中给出的第72条忠告:任何依赖线程调度器来达到正确性或性能要求的程序,很有可能都是不可移植的。当然,世界上没有绝对的事情。

如果程序员一定要规范线程的执行顺序,应该使用线程的同步操作wait(), notify()等显式实现线程之间的同步关系,才能保证程序的正确性。-- 锁机制

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108578566