Java笔记··By/蜜汁炒酸奶

闲聊 Java 源码中的线程创建

在上文《闲聊 Java 线程生命周期》中我们看到 Thread.start() 最终调用 start0 ,另一方面我们也知道 start() 最终调用的是 run() 方法执行的,具体调用过程肯定是在 start0 中了,本篇我们基于 openJDK 21 看看 start0 具体做了些什么事情。

整篇文章细节部分较多,大家也可以直接看最后的总结部分,相对会精简很多。

脑图:Java线程源码层创建

1 注册绑定

通过搜索源码,可知 start0 第一次出现的位置在 jdk21/src/java.base/share/native/libjava/Thread.c 的一个方法映射表中,start0 映射(或者说绑定)的是一个 JVM_StartThread ,当执行一个start0 时,在底层实际会执行 JVM_StartThread 方法,所以想了解 start0 去看JVM_StartThread 就可以了。

static JNINativeMethod methods[] = {  
    {"start0",           "()V",        (void *)&JVM_StartThread},  
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},  
    {"yield0",           "()V",        (void *)&JVM_Yield},  
    {"sleep0",           "(J)V",       (void *)&JVM_Sleep},  
    {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},  
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},  
    {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},  
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},  
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},  
    {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads}, 
    //...
};
1
2
3
4
5
6
7
8
9
10
11
12
13

如何映射的呢?回到 JDK 代码中看 Thread 类,可见其有个静态方法 registerNatives() ,类中的 native 方法通过该方法在类的初始化阶段便被注册了。

public class Thread implements Runnable {  
    /* Make sure registerNatives is the first thing <clinit> does. */  
    private static native void registerNatives();  
    static {  
        registerNatives();  
    }
    // ... 
}
1
2
3
4
5
6
7
8

如果这时候你还没关闭之前的 Thread.c 文件,可以在下面看到这样一个方法:

Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)  
{  
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));  
}
1
2
3
4

以Java开头,包名、类名、方法名以下划线 _ 分隔,最终形成Java_包名_完整类名_方法名 这种命名格式的方法名,这就是JVM中对实现一个本地方法名的相应规定,Java中提供了与其他语言通信的API即 JNI(Java Native Interface),如果想用 Java 调用其他语言(如上面的C)就必须遵守 JNI 中的 API 约定。再比如:

  • JNI 接口指针是本地方法的第一个参数。JNI 接口指针的类型是 JNIEnv。总是使用接口指针 env 来操作 Java 对象。
  • 第二个参数根据本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对象的引用。静态本地方法的第二个参数是对其 Java 类的引用。

更多内容可见《Java Native Interface Specification Contents》,我们这里不再展开。

2 运行

JVM_StartThread 方法在 jdk21/src/hotspot/share/prims/jvm.cpp,新版的代码比之前长,根据是否涉及 CDS 分为了两个 if 分支。

2.1.1 涉及 CDS 时

这部分不是我们关注的重点,只简单说一下,可以直接跳过。

#if INCLUDE_CDS
  if (DumpSharedSpaces) {
    if (log_is_enabled(Info, cds)) {
      ResourceMark rm;
      oop t = JNIHandles::resolve_non_null(jthread);
      log_info(cds)("JVM_StartThread() ignored: %s", t->klass()->external_name());
    }
    return;
  }
1
2
3
4
5
6
7
8
9

在使用 java -Xshare:dump 构建 CDS 时,将忽略多线程的并发执行,因为这期间如果运行多线程并行,会导致符号( symbols )和类( classes )随机顺序加载从而得到的 CDS 存档( archive )存在不确定性( non-deterministic )。

执行 java -Xshare:dump 时最重要的是只运行Java主线程( main Java thread )中创建模块图( module graph )等的代码,此时并未启动主线程。

说了这么多,但 CDS 是什么呢?它的全称是 Class data sharing主要是用来在不同的JVM中共享Class-Data信息,通过使用提前生成好的 jsa 归档文件来启动,减少了class加载的步骤,也就减少了启动时间,从而提升应用程序的启动速度。同时加载到内存中的区域多个JVM可以共享,从而减少了内存占用

早在 JDK 1.5 便引入了 CDS 的概念,当时是将把 rt.jar 这些中的核心类提前转化成内部表示,并转储到一个称为共享存档(shared archive)的文件中,之后可以直接载入内存。默认CDS存档位于 jdk的/server/classes.jsa 。JDK 10 转为开源,OpenJDK 13 / JDK 13 中实现了动态 CDS 归档,提高应用程序类数据共享(AppCDS,Application Class-Data Sharing)的可用性。

2.1.2 不涉及CDS时

#endif 部分代码过长,大家可以从上面 jvm.cpp 处的链接查看,我们选取重点来说,就不再贴全部的源码了。

MutexLocker mu(Threads_lock); :Threads_lock上锁,保证C++的线程对象和操作系统原生线程不会被清除。当前方法执行完,也就是栈帧释放时,会释放这里的锁 。

在《闲聊 Java 线程生命周期》那里我们知道 start() 方法中有个防止同一个线程多次启动的限制。在 JVM 中还有一个底层限制,创建线程对象(设置 JavaThread )和更新线程状态这两个操作是非原子的,操作本身存在窗口期,所以会通过if再次判断,从而保证线程不可重复创建。

if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != nullptr) {  
  throw_illegal_thread_state = true;  
}
// ...
if (throw_illegal_thread_state) {  
  THROW(vmSymbols::java_lang_IllegalThreadStateException());  
}
1
2
3
4
5
6
7

Java 中的 Thread 对象,在 JVM 中是对应的 JavaThread 对象。所以最重要的语句之一就是下面的创建语句:

native_thread = new JavaThread(&thread_entry, sz);
1

sz 是在上面计算得到的线程栈大小,因为使用无符号的线程栈大小,所以不会有负数。

&thread_entry 是传入的运行地址,启动线程,需要一个入口执行点,&thread_entry 这个函数地址便是入口执行点。

static void thread_entry(JavaThread* thread, TRAPS) {  
  HandleMark hm(THREAD);  
  Handle obj(THREAD, thread->threadObj());  
  JavaValue result(T_VOID);  
  JavaCalls::call_virtual(&result,  
                          obj,  
                          vmClasses::Thread_klass(),  
                          vmSymbols::run_method_name(),  
                          vmSymbols::void_method_signature(),  
                          THREAD);  
}
1
2
3
4
5
6
7
8
9
10
11

thread_entry 中的 JavaCalls::call_virtual 是由 JVM 调用 Java 相关方法的函数,这里 vmSymbols::run_method_name() 代表的就是 Java 的 run 方法,在src/hotspot/share/classfile/vmSymbols.hpp 中可找到run_method_name对应关系:

template(run_method_name,                           "run")
1

JavaCalls::call_virtual 的底层是 os::os_exception_wrapper(call_helper, result, method, args, THREAD)os_exception_wrapper 基本上是一个钩子,用于在 Win32 等系统上使用结构化异常处理(线程本地异常过滤器),在 Linux 上不起任何作用,因为 Linux中根本没有做关于异常的相关处理:

 void os::os_exception_wrapper(java_call_t f, JavaValue* value, const methodHandle& method,  
                         JavaCallArguments* args, JavaThread* thread) {  
  f(value, method, args, thread);  
}
1
2
3
4

JavaThread 会创建对应的操作系统线程,具体的我们在后面看。创建关联之后可能由于内存不足不能为 JavaThread 创建 osthread,所以先做判断,如果 osthread 不为空才会通过 prepare 函数将 Java的线程对象 jthread 与 虚拟机 中的 JavaThread 对象关联。

// 不为空说明内存够用,开始通过prepare函数将Java线程对象与 JavaThread  对象关联
if (native_thread->osthread() != nullptr) {  
  // Note: the current thread is not being used within "prepare".  
  native_thread->prepare(jthread);  
}

// ...
// 如果为空,会释放相关资源,清理当前线程,并报出 OOM 异常。
if (native_thread->osthread() == nullptr) {  
  ResourceMark rm(thread);  
  log_warning(os, thread)("Failed to start the native thread for java.lang.Thread \"%s\"",  
                          JavaThread::name_for(JNIHandles::resolve_non_null(jthread)));  
  // No one should hold a reference to the 'native_thread'.  
  native_thread->smr_delete();  
  if (JvmtiExport::should_post_resource_exhausted()) {  
    JvmtiExport::post_resource_exhausted(  
      JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,  
      os::native_thread_creation_failed_msg());  
  }  
  THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),  
            os::native_thread_creation_failed_msg());  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

之后通过 JFR_ONLY(Jfr::on_java_thread_start(thread, native_thread);) 由调用线程确保被调用线程(即新建的线程)拥有有效的 _vm_thread_id 和 _contextual_id。

The starter thread ensures that the startee has a valid _vm_thread_id and _contextual_id.

This is to avoid recursion in thread assignment since accessing the java threadObj can lead to events being fired, a situation the starter thread can handle but not the startee.

最后通过 Thread::start(native_thread); 启动线程,实现对 Java 线程run() 方法的调用。

3 运行中的关键函数

上面是一个大概的执行过程,我们再进入一些关键的函数中看看做了些什么。

3.1 new JavaThread

JVM 的 JavaThread 对象创建具体代码,内部会通过 os::create_thread(this, thr_type, stack_sz); 调用操作系统线程接口创建对应的操作系统线程。

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : JavaThread() {
  _jni_attach_state = _not_attaching_via_jni;
  // 设置入口,入口即上面的 &thread_entry,用于后面 JavaThread::run 函数的执行。
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &CompilerThread::thread_entry ? os::compiler_thread : os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
}
1
2
3
4
5
6
7
8
9
10

3.2 os::create_thread

各系统关于 os::create_thread 的具体实现可见 src/hotspot/os 包,我们这里看下常用的 Linux 版 ,源码太长,涉及各种属性的填充与判断,我们摘取重要部分来看。

最开始时线程状态是 ALLOCATED :

// Initial state is ALLOCATED but not INITIALIZED  
osthread->set_state(ALLOCATED);
1
2

其中最关键的就是调用POSIX Pthreads 线程库的 pthread_create (内部使用 clone 系统调用实现)创建线程一段。

// ...
int ret = 0;  
int limit = 3;  
do {  
  ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);  
} while (ret == EAGAIN && limit-- > 0);
// ... 
1
2
3
4
5
6
7

在下面还有一步等待子线程执行完成的过程,其实就是在等待 pthread_create 创建的线程状态变更为 INITIALIZED 或者因资源不足创建失败。

   // Wait until child thread is either initialized or aborted
    {
      Monitor* sync_with_child = osthread->startThread_lock();
      MutexLocker ml(sync_with_child, Mutex::_no_safepoint_check_flag);
      while ((state = osthread->get_state()) == ALLOCATED) {
        sync_with_child->wait_without_safepoint_check();
      }
    }
1
2
3
4
5
6
7
8

3.2.1 thread_native_entry

这段代码中还有个重要方法就是 thread_native_entry ,这是所有新建线程的启动例程,其实就是 pthread_create 创建的新线程会从 thread_native_entry 这个函数开始执行。

// ...
// 配置 线程本地存储 ThreadLocalStorage ,内部使用 ThreadLocalStorage::set_thread(this);
thread->initialize_thread_current();
// ...
// handshaking with parent thread  
{  
 // 互斥锁
  MutexLocker ml(sync, Mutex::_no_safepoint_check_flag);  
  
  // notify parent thread
  // 设置当前线程状态为 INITIALIZED 初始化完成
  osthread->set_state(INITIALIZED);  
  //  唤醒父级线程,让 os::create_thread 可以正常返回。
  sync->notify_all();  
  
  // wait until os::start_thread()  
  // 初始化后循环等待,直到调用 os::start_thread() 变更线程状态且被唤醒。 
  while (osthread->get_state() == INITIALIZED) {  
    sync->wait_without_safepoint_check();  
  }  
}
// ...
// call one more level start routine
thread->call_run();
// ... 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

被唤醒后执行 thread->call_run() ,内部通过 this->run() 进入 thread_main_inner(),最后在 thread_main_inner 中调用 this->entry_point()(this, this) 通过

JavaCalls::call_virtual(&result,  
                          obj,  
                          vmClasses::Thread_klass(),  
                          vmSymbols::run_method_name(),  
                          vmSymbols::void_method_signature(),  
                          THREAD);  
1
2
3
4
5
6

执行 Java 中的 run 方法完成最终的线程运行。

3.3 JavaThread::prepare

prepare 函数 将 Java的线程对象 jthread 与 虚拟机 中的 JavaThread 对象关联。

void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) {  
  // 检测是否持有锁,只有持有 Threads_lock 才能进行下一步
  assert(Threads_lock->owner() == Thread::current(), "must have threads lock");  
  assert(NoPriority <= prio && prio <= MaxPriority, "sanity check");  
  // 包装当前Java线程对象,将其从 JNI 句柄中获取为 C++ 层的对象并放入一个新句柄thread_oop 中, 之后该句柄可以将转为C++的线程对象传递给其他方法。
  Handle thread_oop(Thread::current(),  
                    JNIHandles::resolve_non_null(jni_thread));  
  assert(InstanceKlass::cast(thread_oop->klass())->is_linked(),  
         "must be initialized");  
  // 借助 thread_oop 句柄将Java层的线程属性等信息设置成C++层 JavaThread 的属性等信息       
  set_threadOopHandles(thread_oop());  
  // 没有优先级时,设置优先级
  if (prio == NoPriority) {  
    prio = java_lang_Thread::priority(thread_oop());  
    assert(prio != NoPriority, "A valid priority should be present");  
  }  
  // 将 Java 优先级下推至本地线程,该过程需要 Threads_lock
  Thread::set_priority(this, prio);  
  // 将当前新增的 JavaThread 类放入到全局线程列表中 
  Threads::add(this);
  // 释放 Threads_lock 锁等
  // 因为 JavaThread* 已经通过 ThreadsList 对 JVM/TI 可见,所以不想等待 Theads_lock 在调用者中的某个地方被释放。
  java_lang_Thread::release_set_thread(thread_oop(), this);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

3.4 Thread::start

该方法在src/hotspot/share/runtime/thread.cpp中,主要作用是调用 set_thread_status 将线程状态变更为 RUNNABLE,同时调用 os::start_thread 启动最终线程。

void Thread::start(Thread* thread) {
  if (thread->is_Java_thread()) {
	  // 设置状态为 RUNNABLE
    java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),JavaThreadStatus::RUNNABLE);
  }
  os::start_thread(thread);
}

1
2
3
4
5
6
7
8

3.4.1 os::start_thread

这段代码在src/hotspot/share/runtime/os.cpp比较简单,就是先设置操作系统线程状态为 RUNNABLE,同时调用对应的 pd_start_thread 实现最终的调用。

void os::start_thread(Thread* thread) {
  OSThread* osthread = thread->osthread();
  osthread->set_state(RUNNABLE);
  pd_start_thread(thread);
}
1
2
3
4
5

在 Java 线程上,当 start_thread 作为 Thread.start 的结果被调用时,Java 线程对象上的操作是同步的。 因此,不会出现竞相启动线程的情况,也就不会出现我们在处理线程时线程退出的情况。

最后调用 pd_start_thread 唤醒 2.3.1 中的等待,执行之后的 run 方法。

void os::pd_start_thread(Thread* thread) {  
  OSThread * osthread = thread->osthread();  
  assert(osthread->get_state() != INITIALIZED, "just checking");  
  // 获得线程的 Monitor 对象,其加锁过程在上面的 os::create_thread 中
  Monitor* sync_with_child = osthread->startThread_lock();  
  // 通过 MutexLocker 获取互斥锁
  MutexLocker ml(sync_with_child, Mutex::_no_safepoint_check_flag);  
  // 唤醒 上面 2.3.1 中阻塞的线程,执行之后的 run 方法。
  sync_with_child->notify();  
}
1
2
3
4
5
6
7
8
9
10

4 总结

最后我们再借助时序图梳理一下整个过程。

图中黑色箭头部分基本都是创建操作系统线程的部分:

  1. 当执行 Thread.start() 后通过 JNI 执行本地方法 start0() 对应的JVM层代码 JVM_StartThread
  2. JVM_StartThread 通过 new JavaThread 创建 JVM 层的 JavaThread 对象,。
    1. 在 JavaThread 中通过 set_entry_point 绑定Java线程的入口方法thread_entry ,后面会通过这个方法调用 Java 代码中的 run() 方法。
    2. 通过 os::create_thread 设置线程状态为 ALLOCATED,调用Linux线程库的 pthread_create 方法从而调用Linux的 clone 系统调用创建一个对应的操作系统线程。同时父线程循环等待,直到新建线程初始化完成(即状态变为 INITIALIZED)或中止(父等待新线程创建这一条未在时序图中展示)。
    3. pthread_create 创建的新线程会从 thread_native_entry 这个函数开始执行 ,会先将线程状态变更为 INITIALIZED,调用 notify_all();唤醒父线程,然后内部循环等待,直到后面调用 os::start_thread() 唤醒后执行最后的 run 方法。

橘黄色箭头部分是将Java线程对象与 JVM 层JavaThread对象关联/绑定的部分:

  1. JVM_StartThread 创建 JVM 层的 JavaThread 对象之后,通过JavaThread::prepare 将 Java的线程对象 jthread 与 虚拟机 中的 JavaThread 对象关联,并将当前新增的 JavaThread 类放入到全局线程列表 ThreadsList 中 。

JVM_StartThread 经过上面黑色箭头部分和黄色箭头部分的这两个步骤,将 Java 层的线程对象与 JVM 层的线程对象以及操作系统的线程对象完成了最终的关联绑定。

蓝色箭头部分是 JVM 通过 Thread::start(native_thread) 执行线程启动部分:

  1. JVM_StartThread 完成线程对象的绑定后,确保资源满足执行后,执行 Thread::start(native_thread) 开始启动线程。
  2. Thread::start(native_thread) 会先将Java线程状态改成 RUNNABLE,之后执行 os::start_thread ,而这个方法会将操作系统线程对象的状态设置成 RUNNABLE,最终通过调用 notify() 唤醒之前 thread_native_entry 中等待的线程。

红色箭头部分是JVM层 notify 通知后,唤醒操作系统线程继续执行 run() 方法的部分。

  1. 当在 thread_native_entry 中等待的线程收到蓝色箭头部分的通知后,判断当前满足继续执行的条件,开始执行 thread->call_run();
  2. thread->call_run(); 会执行 JavaThread::run (在其前后,还有JavaThread::pre_run()JavaThread::post_run() 我们这里就不细说了 ),里面会执行 JavaThread::thread_main_inner
  3. JavaThread::thread_main_inner 会通过 this->entry_point()(this, this); 调用最初的 thread_entry 函数。
  4. thread_entry 函数通过 JavaCalls::call_virtual 函数调用 Java 代码中的 run() 方法。

Java线程执行 start0 的时序图

预览
Loading comments...
0 条评论

暂无数据

example
预览