OpenJDK8的线程本质剖析
线程本质剖析
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
线程本质剖析
1. Java 线程模型的本质:1:1 映射
在现代 Linux 操作系统上的 OpenJDK 8实现中,Java 线程模型的本质是 1:1 映射模型(1:1 Thread Mapping Model)。
这意味着,你在 Java 代码中通过 new Thread() 创建并启动的每一个用户态线程,在 JVM 底层都会对应创建并绑定一个真正的操作系统内核级线程(在 Linux 上表现为通过 clone() 系统调用创建的 pthread)。
1:1 模型的核心特质与系统行为
- 调度权交由内核: JVM 不负责线程的时间片分配、CPU 核心切换或优先级调度。这一切完全由操作系统的调度器(如 Linux 的 CFS 调度器)负责。
- 并发与多核利用: 能够真正利用多核 CPU 的并行计算能力。当某个 Java 线程遭遇阻塞(如等待 I/O 或系统锁)时,Linux 内核会自动挂起该线程并调度其他线程,不会导致整个 JVM 进程阻塞。
- 高昂的资源代价: * 内存开销: 每个线程在创建时,除了 JVM 堆内的
java.lang.Thread对象外,OS 还会为其分配独立的内核栈和用户态栈(通过-Xss配置,默认通常为 1MB)。 - 时间开销: 线程的创建、销毁以及线程上下文切换(Context Switch)都需要从用户态(User Mode)陷入内核态(Kernel Mode),涉及 CPU 寄存器恢复、MMU 页表切换以及 TLB(缓存)失效。
2. OpenJDK 8源码逐层剖析
Java 线程的创建与启动是一个跨越 Java 抽象层 -> JNI 桥梁层 -> JVM 运行时层 -> OS 适配层 的完整纵向调用链路。以下结合 OpenJDK 8源码进行全景透视。
2.1 第一层:Java 核心库层 (java.lang.Thread)
当在 Java 中调用 thread.start() 时,JVM 并不是直接运行 run(),而是通过一个名为 start0() 的 native 方法完成底层内核线程的孵化。
public synchronized void start() {
// 确保线程状态为 "NEW",否则抛出异常(Java 线程不可重复 start)
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 将当前线程加入到所属的线程组中
group.add(this);
boolean started = false;
try {
// 调用底层的本地方法,触发操作系统的线程创建
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* 忽略异常,由主线程抛出原始错误 */
}
}
}
// 核心本地方法,通过 JNI 映射到 JVM 内部的 JVM_StartThread 函数
private native void start0();
2.2 第二层:JNI 桥梁层 (jvm.cpp)
当 Java 层调用 start0() 后,JVM 通过映射表进入本地 C++ 代码。位于 src/share/vm/prims/jvm.cpp 的 JVM_StartThread 是统一入口。
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
// 1. 获取 JVM 内部的全局互斥锁(Threads_lock),确保线程创建过程的线程安全
bool throw_illegal_thread_state = false;
{
MutexLocker mu(Threads_lock);
// 2. 检查底层的 C++ JavaThread 是否已经被创建,防止 Java 端重复调用 start0
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
// 3. 获取 Java 线程对象中定义的栈大小(即通过 Thread 构造函数传入的 stackSize)
jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
size_t sz = size > 0 ? (size_t)size : 0;
// 4. 【核心点】在 JVM 堆外创建一个 C++ 层的 JavaThread 对象
// &thread_entry 是线程启动后的 C++ 入口函数指针(内部会负责回调 Java 的 run() 方法)
native_thread = new JavaThread(&thread_entry, sz);
// 5. 如果底层由于系统内存不足(如无法分配栈空间)导致 JavaThread 创建失败
if (native_thread->osthread() == NULL) {
delete native_thread;
native_thread = NULL;
// 标记抛出 OutOfMemoryError 异常
THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(), "unable to create new native thread");
}
}
}
// 如果状态不合法,抛出异常
if (throw_illegal_thread_state) {
THROW_MSG(vmSymbols::java_lang_IllegalThreadStateException(), "thread status not juvenile");
}
// 6. 将 Java 层的 Thread 对象与 C++ 层的 JavaThread 对象进行双向绑定
java_lang_Thread::set_thread(JNIHandles::resolve_non_null(jthread), native_thread);
// 7. 【关键点】此时线程已经通过 OS 创建成功,但处于挂起初始化状态(ALLOCATED)
// 调用 Thread::start 激活并驱动操作系统真正调度该线程执行
Thread::start(native_thread);
JVM_END
2.3 第三层:JVM 内部线程抽象层 (thread.cpp)
在 src/share/vm/runtime/thread.cpp 中,JavaThread 的构造函数负责初始化 JVM 侧的各种状态标记(如安全点状态、锁状态),并立即向下调用平台相关的 OS 接口。
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_size) :
Thread() {
// 1. 初始化 JavaThread 的内部属性
set_entry_point(entry_point); // 设置刚才传入的 &thread_entry 统一入口
os::ThreadType thr_type = os::java_thread;
// 2. 【核心点】调用平台相关的 os::create_thread 方法,去真正向操作系统申请线程
// 传入 this 指针,使 OS 层的 OSThread 与当前 JavaThread 相互绑定
bool os_alloc_result = os::create_thread(this, thr_type, stack_size);
if (!os_alloc_result) {
// 如果 OS 拒绝分配,则 osthread 将保持为 NULL,上一层会捕获并抛出 OOM
return;
}
// 3. 初始化线程的其他运行时组件(如:TLAB 线程局部分配缓冲区、安全点状态等)
this->set_allocated_thread_state(alloc_allocated);
}
2.4 第四层:操作系统适配层 (os_linux.cpp)
由于 OpenJDK 需要支持跨平台,具体的 OS 线程创建被抽象在 os::create_thread 中。以下是基于 Linux 系统的实现,位于 src/os/linux/vm/os_linux.cpp:
bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {
assert(thread->osthread() == NULL, "caller responsible");
// 1. 分配一个 JVM 内部用于描述 Linux 进程/线程属性的 C++ OSThread 对象
OSThread* osthread = new OSThread(NULL, NULL);
if (osthread == NULL) {
return false; // 内存耗尽,返回失败
}
// 设置线程类型为 java_thread
osthread->set_thread_type(thr_type);
// 2. 初始化线程的状态:此时状态为 ALLOCATED(已分配,未运行)
osthread->set_state(ALLOCATED);
// 将 OSThread 挂载到 JavaThread 对象中
thread->set_osthread(osthread);
// 3. 配置 Linux pthread 线程的属性(如栈大小)
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置为分离状态,死后自动回收资源
// 算计并设置符合 JVM 规范的物理栈大小(结合 -Xss 参数)
stack_size = os::Linux::default_stack_size(thr_type);
if (stack_size > 0) {
pthread_attr_setstacksize(&attr, stack_size);
}
// 4. 【系统调用核心】调用 glibc 的 pthread_create 函数
// - &tid: 接收系统分配的线程 ID
// - &attr: 传入刚才配置好的栈大小等属性
// - java_start: **极其重要**,这是底层 Linux 线程启动后的 Native 入口函数
// - thread: 将当前 JVM JavaThread 对象的指针作为形参传递给 java_start
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);
// 5. 处理 pthread_create 的创建结果
if (ret != 0) {
// 创建失败,释放资源并解除绑定
thread->set_osthread(NULL);
delete osthread;
return false;
}
// Store 操作系统真正的线程 ID 到 OSThread 中
osthread->set_pthread_id(tid);
return true;
}
3. 线程生命周期的闭环:从内核回调到 Java run()
当 os::create_thread 中的 pthread_create 执行成功后,Linux 内核会异步克隆出一个新的线程上下文。
然而,这个新生的 Linux 线程并不能盲目执行。它必须先在 JVM 基础设施中对齐步伐,最终才能回调 Java 层的 Thread.run()。
3.1 阶段一:Glibc 入口激活与同步等待 (java_start)
新线程在 Linux 内核就绪后,首先踩在 src/os/linux/vm/os_linux.cpp 的 java_start 函数里:
static void* java_start(Thread* thread) {
// 1. 此时处于新线程的上下文空间中,强转回 JavaThread 指针
JavaThread *osthread = (JavaThread*)thread;
// 获取当前 Linux 线程的真实 PID (Process ID,在 Linux 1:1 模型中即 LWP 线程 ID)
osthread->osthread()->set_lwp_id(os::Linux::gettid());
// 2. 初始化该线程的操作系统信号处理(如处理 SIGSEGV、SIGQUIT 等)
os::Linux::hotspot_sigmask(thread);
// 3. 【状态同步互斥锁】新线程在此驻留等待
// 因为此时主线程(创建它的那个线程)可能还没把 JavaThread 与 java.lang.Thread 对象完全绑定好
{
Monitor* sync = osthread->osthread()->startThread_lock();
MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);
// 循环检查:直到主线程在 `Thread::start()` 中将状态变更为 INITIALIZED
while (osthread->osthread()->get_state() == ALLOCATED) {
sync->wait(Mutex::_no_safepoint_check_flag);
}
}
// 4. 【突破屏障】状态变为 INITIALIZED,解除阻塞,真正开始执行 JVM 统一线程主体
osthread->run();
return NULL;
}
3.2 阶段二:Java 字节码的激活 (thread_entry)
当主线程调用 Thread::start(native_thread) 后,会将状态从 ALLOCATED 变更为 INITIALIZED 并唤醒上述的 startThread_lock。
新线程醒来,调用 osthread->run(),最终进入 src/share/vm/runtime/thread.cpp 的 JavaThread::run(),并在该方法内部执行先前传入的 thread_entry:
// 这是在 jvm.cpp 中定义的统一入口函数指针
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj()); // 获取对应的 java.lang.Thread 对象
// 准备调用 Java 对象的参数:这里不需要额外参数,只需传入当前 Thread 对象实例
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,
obj, // 目标 Java 对象
KlassHandle(THREAD, SystemDictionary::Thread_klass()), // 目标类:java.lang.Thread
vmSymbols::run_method_name(), // 方法名:"run"
vmSymbols::void_method_signature(), // 方法签名:"()V"
THREAD); // 线程上下文环境
}
通过 JavaCalls::call_virtual,JVM 内部的解释器(Interpreter)或JIT 编译器被激活。它会查找 java.lang.Thread 对象子类中重写的 run() 方法的字节码地址,推入新的栈帧,Java 代码至此全面运行。
4. 系统工程师视角的深度总结
理解了 OpenJDK 8的 1:1 源码模型后,许多高阶线上生产问题的本质便能迎刃而解:
4.1 核心机制对比与参数推导
| 维度 | Java 线程 (java.lang.Thread) |
Linux 线程 (pthread / LWP) |
|---|---|---|
| 映射关系 | 1 | 1 |
| 内存分配 | JVM 堆内存(仅持有一个小对象及状态标量) | 堆外物理内存:内核空间(Task Struct)+ 用户空间(栈内存 -Xss) |
| 调度生命 | 通过 JVM 字节码触发状态流转 | 状态完全由内核 CFS 调度器控制,在 CPU 核心间流动 |
4.2 经典错误:java.lang.OutOfMemoryError: unable to create new native thread
这个经典的错误不是因为 JVM 堆内存(-Xmx)满了,而是因为底层 1:1 模型在向操作系统索要资源时被拒绝。它的本质诱因通常有三:
- 物理内存耗尽: 进程基地址空间中,剩余的堆外物理内存不足以支撑分配一个新的
-Xss要求的栈空间。 - 进程线程数达到了 OS 的限制: 触发了 Linux 系统中
ulimit -u(用户最大进程/线程数)或/proc/sys/kernel/threads-max的硬性阈值。 - cgroups 限制: 在 Docker/K8s 容器环境下,容器内
pids.max限制了当前容器能生成的最大线程分支数。
4.3 线程上下文切换的昂贵代价
由于是 1:1 模型,Java 里的线程状态切换(如 Thread.sleep()、锁竞争失败进入 BLOCKED)在内核层面意味着:
- 当前线程的 CPU 寄存器状态、程序计数器(PC)压入内核栈。
- 内核调度器介入,将该 LWP 移出运行队列。
- 切换 MMU(内存管理单元)的页表,使得新线程的用户空间虚拟内存生效。
- 伴随而来的,是 CPU L1/L2/L3 高速缓存及 TLB 缓存的局部失效,带来可观测的性能局部劣化。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)