1.疑问:Java线程对应的native线程状态如何迁移?

在《【运行时数据区】-并发编程-前置知识(4.并发编程基础)-5》中,我们在JDK层面对Java线程的6种状态迁移进行了实验。

本文再以Java线程从NEW状态迁移到RUNNABLE状态为引子,观测一下JVM为Java线程创建的native线程的状态如何迁移:

2.探索:JVM调用链全景图

为了避免迷失在JVM的源码中,我们先看一下笔者整理的JVM调用链全景:

  • STEP1.JVM准备:JDK通过JNI,加载JVM侧的相关函数。
1
2
3
涉及代码:
(1)JDK:Thread.java的registerNatives()
(2)JVM:jdk/src/share/native/java/lang/Thread.c的registerNatives()
  • STEP2.启动Java线程:JDK侧启动线程,JVM侧进行初始化。
1
2
3
4
涉及代码:
(1)JDK:Thread.java的start()
(2)JVM:src/share/vm/prims/jvm.cpp的JVM_StartThread()
(3)JVM:src/share/vm/runtime/thread.cpp的JavaThread()
  • STEP3.记录JDK回调:JVM侧记录JDK侧的Java线程回调函数
1
2
涉及代码:
(1)JVM:src/share/vm/runtime/thread.hpp的set_entry_point()
  • STEP4.创建native线程:JVM侧创建native线程,初始化后等待CPU执行这个native线程
1
2
涉及代码:
(1)JVM:src/os/linux/vm/os_linux.cpp的create_thread()
  • STEP5.等待:CPU执行native线程,JVM阻塞,等待native线程进入RUNNABLE状态
1
2
涉及代码:
(1)JVM:src/os/linux/vm/os_linux.cpp的java_start()
  • STEP6.打破循环:JVM在STEP4后,做了其它操作,最终设置native线程、Java线程为RUNNABLE状态,打破STEP5等待
1
2
3
4
5
涉及代码:
(1)JVM:src/share/vm/prims/jvm.cpp的JVM_StartThread()
(2)JVM:src/share/vm/runtime/thread.cpp的start()
(3)JVM:src/share/vm/runtime/os.cpp的start_thread()
(4)JVM:src/os/linux/vm/os_linux.cpp的pd_start_thread()
  • STEP7.回调JDK:JVM通过JNI回调JDK侧的Java线程回调
1
2
3
涉及代码:
(1)JVM:src/share/vm/runtime/thread.cpp的run()
(2)JVM:src/share/vm/runtime/thread.cpp的thread_main_inner()

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.Threadstart方法核心是调用了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.cppJavaThread的构造函数中调用os_linux.cppcreate_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.cppjava_start函数是native线程的回调函数。

  • 这个函数的结束处,native线程的状态一直处于INITIALIZED状态,这个状态就对应Java线程的NEW状态。此时,native线程阻塞。
  • native线程被打破了INITIALIZED状态,native线程不再原地止步,而是进一步执行thread.cpprun方法。

8.细节6:打破循环

第六步,我们再来理解JVM如何打破上一步native线程止步不前的状态。

  • jvm.cpp的JVM_StartThread方法在创建了JavaThread对象后,调用了thread.cppstart函数。

  • thread.cppstart方法将JavaThread对象设置为RUNNABLE状态,此时JDK对应的Java线程处于RUNNABLE状态。
  • thread.cpp调用os.cppstart_thread函数。

  • os.cppstart_thread函数将native线程也设置为RUNNABLE状态,此时Java线程和native线程都处于RUNNABLE状态。

  • os.cppstart_thread函数进一步调用os_linux.cpppd_start_thread函数。

9.细节7:回调JDK

第七步,在第六步打破了native线程止步不前的状态后,native线程调用thread.cppthread_main_inner函数。

  • native线程调用thread.cppthread_main_inner函数。

  • thread.cppthread_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……),这种感觉令人自在、平静、喜乐。