前言

本文旨在记录近期研读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.cppJVM_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.cppjava_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.cppJavaThread::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 模型在向操作系统索要资源时被拒绝。它的本质诱因通常有三:

  1. 物理内存耗尽: 进程基地址空间中,剩余的堆外物理内存不足以支撑分配一个新的 -Xss 要求的栈空间。
  2. 进程线程数达到了 OS 的限制: 触发了 Linux 系统中 ulimit -u(用户最大进程/线程数)或 /proc/sys/kernel/threads-max 的硬性阈值。
  3. cgroups 限制: 在 Docker/K8s 容器环境下,容器内 pids.max 限制了当前容器能生成的最大线程分支数。

4.3 线程上下文切换的昂贵代价

由于是 1:1 模型,Java 里的线程状态切换(如 Thread.sleep()、锁竞争失败进入 BLOCKED)在内核层面意味着:

  1. 当前线程的 CPU 寄存器状态、程序计数器(PC)压入内核栈。
  2. 内核调度器介入,将该 LWP 移出运行队列。
  3. 切换 MMU(内存管理单元)的页表,使得新线程的用户空间虚拟内存生效。
  4. 伴随而来的,是 CPU L1/L2/L3 高速缓存及 TLB 缓存的局部失效,带来可观测的性能局部劣化。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐