OpenJDK为什么采用JVM线程和内核线程1:1的模型?

Posted 沙雕程序猿的日常

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenJDK为什么采用JVM线程和内核线程1:1的模型?相关的知识,希望对你有一定的参考价值。

众所周知的是,在Linux平台上,OpenJDK采用的是One-to-One线程模型。也就是,每一个JVM线程,都有一个对应的内核线程。这篇文章就是试图讨论一下,为什么会做出这样一种设计。

在上古年代,或者说在众多JVM的实现中,并不是所有的JVM实现都是采用这种模型的。比如说在早期的Solaris上,JVM的实现采用的是Many-to-One模型。也就是说,所有的JVM线程对应到了一个内核线程上。

除了这里提到了One-to-One线程模型和Many-to-One线程模型,还有一种Many-to-Many模型。这种模型是将M个用户线程映射到N个内核线程上。一般而言,M是大于N的。后面Solaris的线程模型就切换到了Many-to-Many模型上。
用Solaris的例子来感受一下。


Many-to-One

OpenJDK为什么采用JVM线程和内核线程1:1的模型?

One-to-One

OpenJDK为什么采用JVM线程和内核线程1:1的模型?

Many-to-Many

所以,“OpenJDK为什么使用One-to-One模型”的答案实际上就是这三种模型优缺点的对比。

Many-to-One

使用Many-to-One模型,在早些年看来,是一个比较不错的决定。首要的一个理由是,这种模型比较“成熟”,除了JVM,还要很多东西也是采用这种模型。而且在那个CPU核数不多的年代,"Many-to-One"模型在单核平台上,与其余模型比起来,反而有一些性能优势。

但是这种模型在多核CPU上会带来严重的性能问题,即该模型只能运行在其中的一个核上,完全无法利用多核CPU。

另外一个问题是,用户级线程相互之间会干扰。一种显而易见的情况是,某个Java线程执行了某个系统调用,而导致内核线程阻塞的时候,其余的线程,也会全部被阻塞掉。

这个东西就有点像TCP多路复用带来的问题。Stream1丢失了某个包,而将Stream2, Stream3全部阻塞了。

Many-to-Many

随着硬件的发展和内核的更新换代。Many-to-Many模型和One-to-One模型变得更加常见。

Many-to-Many模型有效的解决了无法利用多核CPU的问题。也就是说,给JVM带来了真正的并行。JVM线程被绑定到不同的内核线程上,这些内核可以分别被不同的CPU核所调度。

但是线程之间相互影响的问题还是没有解决。也就是,某几个JVM线程被映射到一个内核线程后,如果这里面的一个JVM线程发起系统调用导致内核线程阻塞,那么剩下的几个线程依旧会被阻塞。

One-to-One

这是我们最为熟知的一种模型。它解决了无法利用多核CPU和线程相互之间影响两个问题。

在这种模型之下,JVM直接依赖于平台的线程库。包括线程调度、通信之类的,几乎都依赖于内核线程的机制。JVM线程更加多的看做是一个内核线程的包装,有点语法糖的意思。因为每一个JVM线程都绑定到了一个内核线程,所以即便一个线程阻塞,也不会影响别的线程

性能问题

最后来比较一下性能。当然我这个菜鸡是没有那个能耐来做实验的,我来引用一下别人做的实验的数据。这些数据是在Linux平台上,采用Green Thread和内核线程的JVM的实现的对比。这部分内容引用自《Comparative performance evaluation of Java threads
for embedded applications: Linux Thread vs. Green Thread》


OpenJDK为什么采用JVM线程和内核线程1:1的模型?

上下文切换

OpenJDK为什么采用JVM线程和内核线程1:1的模型?

这是使用yield()时候上下文切换的性能对比。可以看到的是,在线程数不多的时候,使用内核线程的性能甚至稍微要比使用用户级线程的差一点。而在线程多了以后,使用内核线程有10%的性能提高。

这个是有点违反直觉的。因为一般的想法是,用户级线程的切换,应该要比内核线程的上下文调度要快。我也不知道该怎么解释这个结果……

线程控制

OpenJDK为什么采用JVM线程和内核线程1:1的模型?

这个结果就十分符合我们的预期。使用内核线程的时候,无论是线程创建还是调度,都依赖于系统调用。相比之下,使用用户级线程就没有这种烦恼。系统调用的低效和昂贵,也一览无余了。

线程同步

OpenJDK为什么采用JVM线程和内核线程1:1的模型?

这部分性能也是这样,用户级线程拥有很大的优势。

IO阻塞

这个我觉得并没有太大的参考价值。因为用户级线程使用的是非阻塞IO,避开了线程阻塞这个天坑。不过即便如此,它还是要稍微比内核线程慢一点。

代码执行

用户级线程也是明显占优。

总结

我来总结一下。如果按照最后的性能分析来看,那么显然OpenJDK使用One-to-One的模型是有点傻气的。不过我对这个性能测试持有一种怀疑的态度。

但是,其实现在有一种趋势,就是协程。协程很大程度上就是为了避免线程调度的开销,试图在应用层面上解决这种问题。所以这种解决问题的思路,就比较贴近用户级线程。在Kotlin1.3中,已经正式支持协程了。等有空我来分析一下Kotlin协程的实现。


以上是关于OpenJDK为什么采用JVM线程和内核线程1:1的模型?的主要内容,如果未能解决你的问题,请参考以下文章

JVM线程与Linux内核线程的映射(关系)

openjdk-alpine镜像无法打印线程堆栈和内存堆栈问题

JVM:线程的实现

什么时候应该为JVM采用线程转储

jvm 线程实现机制

内核线程Java线程与内核线程区别