近期准备给大家分享专题系列文章,聚焦Java多线程机制。会从hotspot源码角度,给大家揭秘平时学习多线程那些从来没有想过的问题,或者存在疑虑却又无法证明的理论。
今天是系列文章首篇,咱们来谈谈Java线程创建的一些细节问题:
-
Java线程是如何与OS线程建立联系的
-
Java线程与OS线程共用一套线程状态吗
-
Java线程是如何做到创建与启动分开的
-
Java线程在JVM层面为什么要有JavaThread与OSThread
-
Java线程为什么设计的时候要将创建与启动分开
先把这五个问题搞明白吧,其他的后面的文章再分享。本篇文章的观点聚焦于Linux平台,不适用于所有平台。因为不同OS,底层差异还是挺大的。
Linux线程创建
线程能力是操作系统才有的,固Java的线程机制一定是基于OS的线程机制实现的,加上些许JVM自身的考虑在其中。这些考虑在哪能看到?JSR规范中。
上一段Linux平台下创建线程的代码
这样创建出来的线程,一般称为原生线程,或native thread。Java的线程实现其实就是将Linux下的线程机制基于JSR规范进行设计重组。如果我们了解Linux的线程机制,并搞明白了JVM是如何进行设计重组的。那么,Java的多线程,我们就算真正学明白了。
可以发现,跟Java创建线程明显不同的是:原生线程创建与运行是一体的,即线程创建完毕马上就运行。而Java中创建归创建,调用start线程才运行。
Java线程与原生线程之间是这样的关系:Thread对象->JavaThread->OSThread->native thread。下文会结合hotspot源码细讲。
Java线程创建
上一段创建Java线程的代码
从研究问题的角度,这段我们习以为常的代码要分成两部分来看:一、创建一个Java对象。注意,这一步只是单纯地创建一个Java对象,并没有什么特殊处理在里面。二、调用start方法让线程运行。我们上面提的几个问题,所有的秘密都在这一步中。接下来我结合hotspot源码把我对上面几个问题的思考分享给大家。
第一个问题:Java线程是如何与OS线程建立联系的,先上代码,这里只保留了关键代码
分析下这段代码:
-
第2句创建了一个OSThread对象。第4句将JavaThread与OSThread建立联系。
-
第16句是创建原生线程。这一步执行完,新创建的线程就会马上执行java_start方法,java_start方法最终会通过JNI调用Java代码中的run方法。
-
第18句是将原生线程的ID存储到OSThread中。因为Linux下所有操作线程的API都需要传入线程ID。
-
总结来说,Java线程与原生线程之间是这样的关系:Thread对象->JavaThread->OSThread->native thread。
第二个问题:Java线程与OS线程共用一套线程状态吗?不是。JavaThread中有线程状态,OSThread也有线程状态。这个问题后面写篇文章细讲。
第三个问题:Java线程是如何做到创建与启动分开的?答案是借助锁。研究过Hotspot源码的小伙伴可能知道,这里的锁要么是parker对象, 要么是ParkEvent。这两个锁对象是理解多线程不可逾越的横沟,后面写篇文章细讲。
如果你对Java线程创建的细节能掌握到这个程度,理论上来说面试基本不会被虐。我再给你一些面试装叉的灵丹妙药。
分离线程
大家有没有注意到os::create_thread中的第8句代码。这句代码是设置即将创建的线程属性,值为PTHREAD_CREATE_DETACHED。这个值的意思是以分离状态创建线程。即我们通过new Thread创建的线程于OS而言都是分离线程。
为什么要创建分离线程,而不是普通线程呢?这就要说到分离线程的好处。也不得不感叹写JVM的大佬对硬件、对OS的精通程度。
在我们使用默认属性创建一个线程的时候,线程是 joinable 的。joinable 状态的线程,必须在另一个线程中使用 pthread_join() 等待其结束, 如果一个 joinable 的线程在结束后,没有使用 pthread_join() 进行操作, 这个线程就会变成"僵尸线程"。每个僵尸线程都会消耗一些系统资源, 当有太多的僵尸线程的时候,可能会导致创建线程失败,因为每个进程能够持有的描述符是有限的。
当线程被设置为分离状态后,线程结束时,它的资源会被系统自动的回收, 而不再需要在其它线程中对其进行 pthread_join() 操作。
Linux系统层面的线程知识,我在我的手写JVM二期班中都会给大家补上。
我的困惑
Java的线程机制我觉得设计的过于复杂了,两个方面:一、需要维护两套线程状态;二、创建与启动分开。
目前不太理解JVM为什么要这样设计,所以我在手写JVM的课程中,我准备反其道而行,看看这样实现存在什么样的问题或者不便,这些问题或不便应该就是JVM这样设计的理由。这也就是我开设手写JVM小班的意义之一,你看到的所有理论,你心中的所有猜疑,如果你有一个自己手写自己熟悉的JVM,你就可以随时去论证,找到正确答案。而不是模棱两可、自己都没有底气的猜疑。
有些小伙伴可能想,我直接改hotspot源码不也可以吗?的确,可以,但是这个门槛太高了。你如果没有能力手写一个JVM,改hotspot源码那就是天方夜谭了。
结语
总是听到小伙伴说:多线程好难学、多线程学不会、多线程没概念……为什么会这样呢?我觉得可能是这样:大多数小伙伴接触线程都是从高级语言开始的,比如Java。而高级语言的线程是基于操作系统的线程机制实现的,而小伙伴们没有学过操作系统应用层的学习机制,更不了解操作系统内核级的线程机制,所以学完多线程有一种似会非会的感觉。跟别人交流或者面试的时候,滥用应用态切内核态、线程调度等名词。写多线程程序更是惨不忍睹。
我的手写JVM小班新增的内容就是以我所理解的方式让大家精通多线程。学起来会有一点难度,所以如果你没有决心想成为一个技术大牛,建议有这个决心的时候再来,我随时都在。当然啦,底层的学习会比较枯燥,那我是怎么解决这个问题的呢?手写。沉浸在设计者的角度去思考去实现,随着对知识点本身的一点点领悟,代码的一点点成型,那种成就感是CRUD无法比拟的。给大家看下二期新增的知识大纲。
如果你对JVM底层感兴趣,想要领先身边人技术一个level,欢迎加入我的手写JVM底层班,带你一起领略计算机底层的风采。