1.疑问:Java线程对应的native线程状态如何迁移?
在《【运行时数据区】-并发编程-前置知识(4.并发编程基础)-5》中,我们在JDK层面对Java线程的6种状态迁移进行了实验。
本文再以Java线程从NEW状态迁移到RUNNABLE状态为引子,观测一下JVM为Java线程创建的native线程的状态如何迁移:
-6/image-20230413160245714.png)
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线程回调
| |
-6/image-20230414064052581.png)
3.细节1:JVM准备
第一步,看JVM如何将java.lang.Thread中用到的native方法批量注册的:
-6/image-20230414060903544.png)
- 在JDK中,
java.lang.Thread在静态代码块中调用了registerNatives。此方法为native方法。
-6/image-20230414061036368.png)
- 在JVM中,java/lang/Thread.c定义了JNI方法
Java_java_lang_Thread_registerNatives。此方法使用了methods变量,此变量定义了java.lang.Thread类中所有的native方法对应的函数指针。
-6/image-20230414061109828.png)
4.细节2:启动Java线程
第二步,理解java.lang.Thread调用start方法时,JVM对应的处理:
-6/image-20230414061547200.png)
java.lang.Thread的start方法核心是调用了native方法start0。
-6/image-20230414061148420.png)
- 在JVM中,native的
start0方法对应的实现为JVM_StartThread函数。
-6/image-20230414061324005.png)
JVM_StartThread方法核心是创建了JavaThread对象。此对象是JVM中表示Java线程对象的抽象。
-6/image-20230414061356033.png)
5.细节3:记录JDK回调
第三步,理解JVM如何记录Java线程的回调:
-6/image-20230414061824024.png)
- 在JVM中,thread.cpp中定义了
JavaThread的构造函数,第一个参数entry_point就表示Java线程的回调函数。
-6/image-20230414061653606.png)
- thread.hpp提供了
set_entry_point函数,JavaThread的构造函数调用此函数,将Java线程的回调记录到内存中,供后续流程调用。
-6/image-20230414061709047.png)
6.细节4:创建native线程
第四步,在JVM做好一切准备工作后,JVM如何在操作系统上创建native线程:
-6/image-20230414064223932.png)
- JVM的
thread.cpp在JavaThread的构造函数中调用os_linux.cpp的create_thread函数。 create_thread函数创建了OSThread对象,此对象记录了JavaThread和即将创建的native线程之间的一一对应关系。
-6/image-20230414061900504.png)
- 在建立了
JavaThread对象和native线程对象后,JVM就调用pthread库的pthread_create方法,在操作系统上创建了真正的native线程。
-6/image-20230414061913975.png)
- 到这里,JDK中的Java线程仍然处于
NEW状态,而JVM中的native线程在一段时间内处于ALLOCATED状态,JVM一直会等待native线程突破这个状态。并且,当pthread库在操作系统层创建native线程出现问题时,native线程处于ZOMBIE状态。
-6/image-20230414061932717.png)
7.细节5:等待
第五步,在操作系统层面已经存在了和JDK的Java线程一一对应的native线程,那么我们就要来理解在CPU下一个可能的时间周期中是如何执行的native线程的回调。
-6/image-20230414064342579.png)
os_linux.cpp的java_start函数是native线程的回调函数。
-6/image-20230414064407101.png)
- 这个函数的结束处,
native线程的状态一直处于INITIALIZED状态,这个状态就对应Java线程的NEW状态。此时,native线程阻塞。 - 当
native线程被打破了INITIALIZED状态,native线程不再原地止步,而是进一步执行thread.cpp的run方法。
-6/image-20230414064418955.png)
8.细节6:打破循环
第六步,我们再来理解JVM如何打破上一步native线程止步不前的状态。
-6/image-20230414065410715.png)
- 在
jvm.cpp的JVM_StartThread方法在创建了JavaThread对象后,调用了thread.cpp的start函数。
-6/image-20230414065209484.png)
thread.cpp的start方法将JavaThread对象设置为RUNNABLE状态,此时JDK对应的Java线程处于RUNNABLE状态。thread.cpp调用os.cpp的start_thread函数。
-6/image-20230414065229125.png)
os.cpp的start_thread函数将native线程也设置为RUNNABLE状态,此时Java线程和native线程都处于RUNNABLE状态。
-6/image-20230414065247151.png)
os.cpp的start_thread函数进一步调用os_linux.cpp的pd_start_thread函数。
-6/image-20230414065304780.png)
9.细节7:回调JDK
第七步,在第六步打破了native线程止步不前的状态后,native线程调用thread.cpp的thread_main_inner函数。
-6/image-20230414064752160.png)
native线程调用thread.cpp的thread_main_inner函数。
-6/image-20230414064845782.png)
-6/image-20230414064858699.png)
thread.cpp的thread_main_inner函数中,进一步调用了在第三步中保存的Java线程的回调entry_point。
-6/image-20230414064911172.png)
- 在
jvm.cpp中,thread_entry函数调用了JavaCalls::call_virtual函数,这个函数会通过JNI反向调用JDK中的Java代码,此处就是回调了JDK中的Java线程回调函数。
-6/image-20230414064925823.png)
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调用执行。
-6/image-20230414070017806.png)
11.随想
每次分析JVM源码,仿佛在一个生活在三维空间的生物(Java程序猿),窥探到四维空间(JVM、操作系统、CPU……),这种感觉令人自在、平静、喜乐。
-6/image-20230415010840596.png)