1.疑问:Java线程对应的native线程状态如何迁移?
在《【运行时数据区】-并发编程-前置知识(4.并发编程基础)-5》中,我们在JDK层面对Java线程的6种状态迁移进行了实验。
本文再以Java线程从NEW状态迁移到RUNNABLE状态为引子,观测一下JVM为Java线程创建的native线程的状态如何迁移:
2.探索:JVM调用链全景图
为了避免迷失在JVM的源码中,我们先看一下笔者整理的JVM调用链全景:
- STEP1.JVM准备:JDK通过JNI,加载JVM侧的相关函数。
|
|
- STEP2.启动Java线程:JDK侧启动线程,JVM侧进行初始化。
|
|
- STEP3.记录JDK回调:JVM侧记录JDK侧的Java线程回调函数
|
|
- STEP4.创建native线程:JVM侧创建native线程,初始化后等待CPU执行这个native线程
|
|
- STEP5.等待:CPU执行native线程,JVM阻塞,等待native线程进入RUNNABLE状态
|
|
- STEP6.打破循环:JVM在STEP4后,做了其它操作,最终设置native线程、Java线程为RUNNABLE状态,打破STEP5等待
|
|
- STEP7.回调JDK:JVM通过JNI回调JDK侧的Java线程回调
|
|
3.细节1:JVM准备
第一步,看JVM如何将java.lang.Thread
中用到的native方法批量注册的:
- 在JDK中,
java.lang.Thread
在静态代码块中调用了registerNatives
。此方法为native方法。
- 在JVM中,java/lang/Thread.c定义了JNI方法
Java_java_lang_Thread_registerNatives
。此方法使用了methods
变量,此变量定义了java.lang.Thread
类中所有的native方法对应的函数指针。
4.细节2:启动Java线程
第二步,理解java.lang.Thread
调用start
方法时,JVM对应的处理:
java.lang.Thread
的start
方法核心是调用了native方法start0
。
- 在JVM中,native的
start0
方法对应的实现为JVM_StartThread
函数。
JVM_StartThread
方法核心是创建了JavaThread
对象。此对象是JVM中表示Java线程对象的抽象。
5.细节3:记录JDK回调
第三步,理解JVM如何记录Java线程的回调:
- 在JVM中,thread.cpp中定义了
JavaThread
的构造函数,第一个参数entry_point
就表示Java线程的回调函数。
- thread.hpp提供了
set_entry_point
函数,JavaThread
的构造函数调用此函数,将Java线程的回调记录到内存中,供后续流程调用。
6.细节4:创建native线程
第四步,在JVM做好一切准备工作后,JVM如何在操作系统上创建native线程:
- JVM的
thread.cpp
在JavaThread
的构造函数中调用os_linux.cpp
的create_thread
函数。 create_thread
函数创建了OSThread
对象,此对象记录了JavaThread
和即将创建的native线程
之间的一一对应关系。
- 在建立了
JavaThread
对象和native线程对象
后,JVM就调用pthread
库的pthread_create
方法,在操作系统上创建了真正的native线程
。
- 到这里,JDK中的Java线程仍然处于
NEW
状态,而JVM中的native线程
在一段时间内处于ALLOCATED
状态,JVM一直会等待native线程
突破这个状态。并且,当pthread
库在操作系统层创建native线程
出现问题时,native线程
处于ZOMBIE
状态。
7.细节5:等待
第五步,在操作系统层面已经存在了和JDK的Java线程一一对应的native线程
,那么我们就要来理解在CPU下一个可能的时间周期中是如何执行的native线程
的回调。
os_linux.cpp
的java_start
函数是native线程
的回调函数。
- 这个函数的结束处,
native线程
的状态一直处于INITIALIZED
状态,这个状态就对应Java线程的NEW
状态。此时,native线程
阻塞。 - 当
native线程
被打破了INITIALIZED
状态,native线程
不再原地止步,而是进一步执行thread.cpp
的run
方法。
8.细节6:打破循环
第六步,我们再来理解JVM如何打破上一步native线程
止步不前的状态。
- 在
jvm.cpp
的JVM_StartThread方法在创建了JavaThread
对象后,调用了thread.cpp
的start
函数。
thread.cpp
的start
方法将JavaThread
对象设置为RUNNABLE
状态,此时JDK对应的Java线程处于RUNNABLE
状态。thread.cpp
调用os.cpp
的start_thread
函数。
os.cpp
的start_thread
函数将native线程
也设置为RUNNABLE
状态,此时Java线程和native线程
都处于RUNNABLE
状态。
os.cpp
的start_thread
函数进一步调用os_linux.cpp
的pd_start_thread
函数。
9.细节7:回调JDK
第七步,在第六步打破了native线程
止步不前的状态后,native线程
调用thread.cpp
的thread_main_inner
函数。
native线程
调用thread.cpp
的thread_main_inner
函数。
thread.cpp
的thread_main_inner
函数中,进一步调用了在第三步中保存的Java线程的回调entry_point
。
- 在
jvm.cpp
中,thread_entry
函数调用了JavaCalls::call_virtual
函数,这个函数会通过JNI反向调用JDK中的Java代码,此处就是回调了JDK中的Java线程回调函数。
10.结论
通过前述代码流程的分析,我们可以得到如下结论:
- JDK中,
new java.lang.Thread()
会创建Java线程,此时Java线程处于NEW
状态。 - JDK中,调用Java线程的
start
方法后,在JVM中创建native线程
- JVM中,
native线程
先后经历ALLOCATED
状态,也可能出现ZOMBIE
状态。 - JVM中,
native线程
进化为INITIALIZED
状态,可以对标JDK中Java线程的NEW
状态。 - JVM中,
native线程
被打破等待循环后,native线程
变迁为RUNNABLE
状态。 - JDK中,
Java线程
也变迁为RUNNABLE
状态,Java线程
的回调函数也被JVM调用执行。
11.随想
每次分析JVM源码,仿佛在一个生活在三维空间的生物(Java程序猿),窥探到四维空间(JVM、操作系统、CPU……),这种感觉令人自在、平静、喜乐。