用户_核心态 3

  1. 在系列文章一说明用户线程、内核线程的映射关系
  1. 在系列文章二说明用户态到核心态的转化

本章主要说明 jvm 中线程与核心线程的关系即: 一个 jvm 线程对应一个内核线程 ( pthread )。内核线程中存在用户栈和内核栈,访问不同区域是会进行用户态到内核态的切换。另外线程的本质依然是对资源的计算、调度,因此在没有实现内核线程的日子,cpu 是直接操作用户线程,用户线程需要被进程管理分批交给 cpu 运算,也就是 java 1.2 中的 green thread。

① 在用户空间中实现线程 ( cpu 是直接操作用户线程 )

在早期的操作系统中,所有的线程都是在用户空间下实现的,操作系统只能看到线程所属的进程,而不能看到线程。
image.png
从我们开发者的角度来理解用户级线程就是说:在这种模型下,**我们需要自己定义线程的数据结构、创建、销毁、调度和维护等,**这些线程运行在操作系统的某个进程内,然后操作系统直接对进程进行调度。
这种方式的好处一目了然,首先第一点,就是即使操作系统原生不支持线程,我们也可以通过库函数来支持线程;第二点,线程的调度只发生在用户态,避免了操作系统从内核态到用户态的转换开销。
当然缺点也很明显:由于操作系统看不见线程,不知道线程的存在,而 CPU 的时间片切换是以进程为维度的,所以如果进程中某个线程进行了耗时比较长的操作,那么由于用户空间中没有时钟中断机制,就会导致此进程中的其它线程因为得不到 CPU 资源而长时间的持续等待;另外,如果某个线程进行系统调用时比如缺页中断而导致了线程阻塞,此时操作系统也会阻塞住整个进程,即使这个进程中其它线程还在工作。

② 在内核空间中实现线程 ( 对比 )

所谓内核级线程就是运行在内核空间的线程, 直接由内核负责,只能由内核来完成线程的调度。
几乎所有的现代操作系统,包括 Windows、Linux、Mac OS X 和 Solaris 等,都支持内核线程。
每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。
从我们开发者的角度来理解内核级线程就是说:我们可以直接使用操作系统中已经内置好的线程,线程的创建、销毁、调度和维护等,都是直接由操作系统的内核来实现,我们只需要使用系统调用就好了,不需要像用户级线程那样自己设计线程调度等。
image.png
上图画的是 1:1 的线程模型,所谓线程模型,也就是用户线程和内核线程之间的关联方式,线程模型当然不止 1:1 这一种, 参考第一章。

其他解释

创建一个Java线程的路径:


我们模糊掉内部的控制逻辑,首先聚焦于线程创建的主流程:

创建不同层级的线程对象(由Java语言层向下传递)

创建Java语言层面的Thread对象,调用start()方法后;会调用JVM的JVM_StartThread()方法创建JavaThread对象,并按照当前虚拟机所处的操作系统创建不同的OSThread对象(操作系统核心级线程);

不同层级的线程依次启动(从OS层开始向上传递)

核心级线程创建成功后,会调用入口函数:thread_native_entry方法,该方法会调用JavaThread::run方法启动JVM线程,JVM线程启动后会调用Thread对象传入的thread_entry方法,该方法会调用Thread::run方法-即我们代码中定义的该线程需要执行的代码块;

由Java语言层向下传递

Java语言层

在Java语言中,我们手动创建线程时,会通过类似下面的方式启动:

// 定义Thread对象(提供线程run()方法的实现)
Thread runnableThread = new Thread(() -> {
    System.out.println("[runnableThread] begin start");
    System.out.println("[runnableThread] end start");
});

// 启动定义的Thread (如果我们手动调用Thread.run(),并不会出现上述的日志答打印)
runnableThread.start();

start0()方法需要在映射表(native方法 -> JVM方法的映射) 中找到对应的的JVM层的方法,而Thread.java中的static代码块调用 registerNatives()方法初始化了映射表:

public synchronized void start() {
    /**
     * Thread状态 : 0 -> "NEW"
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

boolean started = false;
    try {
        // native 方法
        start0();
        started = true;
    }
}

private native void start0();

于是随着start0()对应的JVM_StartThread这个JVM方法被执行,代码的层次也从Java语言层向下传递到了JVM层;

JVM层

JVM_StartThread

JVM_StartThread方法主要是通过Java语言层的Thread对象中传递的信息,创建了JavaThread这个JVM线程对象:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JavaThread *native_thread = NULL;
  // 通过bool量表示是否有异常,因为下面持有 Threads_lock 锁时,抛出异常会导致锁不能被释放
  bool throw_illegal_thread_state = false;
  
  {
    // 获取 Threads_lock 这个JVM内部的 Mutex锁
    MutexLocker mu(Threads_lock);

    // 如果线程已经创建成功,则不可以重复创建
    // PS: 不可以通过Thread状态机判断,因为创建线程成功与修改线程状态这两个操作是非原子的,存在窗口期
    if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
      throw_illegal_thread_state = true;
    } else {
      // 获取Thread中定义的栈大小,一般我们创建Thread对象时不会显式指定传入stackSize对象
      // 未指定时,后面在创建栈时会使用 -Xss 这个JVM参数指定的栈大小
      jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
     
      // 创建JVM线程(用JavaThread对象表示)
      // thread_entry 为 Thread::run
      native_thread = new JavaThread(&thread_entry, sz);
     
      ...

JVM_END

构建JavaThread对象

构建JavaThread对象,通过glibc库方法pthread_create创建内核级线程,并将JavaThread(JVM)层线程对象与操作系统内核级线程关联起来;
以Linux系统为例,一个内核级线程会对应一个JavaThread(一对一);

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread() {
  // entry_point 线程创建成功后的入口代码 (这里是Java语言层面传入的 Thread.run()方法)
  set_entry_point(entry_point);
  // 创建的Thread类型为javaThread
  os::ThreadType thr_type = os::java_thread;

  // 调用不同操作系统的创建线程方法创建线程
  os::create_thread(this, thr_type, stack_sz);
}

/**
 * JVM为每一种操作系统(linux、bsd...)都写了对应的创建线程的OS方法 
 * 下文以Linux为例
 */
bool os::create_thread(Thread* thread, ThreadType thr_type,
                       size_t req_stack_size) {

  // 操作系统线程对象
  OSThread* osthread = new OSThread(NULL, NULL);
  if (osthread == NULL) {
    return false;
  }
  
  // 将JavaThread(JVM)层线程对象与操作系统内核级线程关联起来,
  // 这样就可以通过使用JavaThread操作内核级线程 (一对一的关系)
  thread->set_osthread(osthread);
  {
    // 值-结果变量
    pthread_t tid;
    // 通过pthread_create方法创建内核级线程
    // thread_native_entry 这个是内核级线程的 start_routine(线程创建成功后会调用)
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);

    // 操作系统 tid (轻量级进程Id)
    osthread->set_pthread_id(tid);
  }
  return true;
}

OS 层 (通过glibc进行调用)

glibc中的pthread_create方法主要是创建一个OS内核级线程,我们不深入细节,主要是为该线程分配了栈资源;需要注意的是这个栈资源对于JVM而言是堆外内存,因此堆外内存的大小会影响JVM可以创建的线程数。
我们知道对于内核级线程,操作系统会为其创建一套栈:用户栈+内核栈,其中用户栈工作在用户态,内核栈工作在内核态,在发生系统调用时,线程的执行会从用户栈切换到内核栈;
那么这一套栈与JVM规范中定义的JVM栈+本地方法栈有什么联系呢? 在JVM概念中,JVM栈用来执行Java方法,而本地方法栈用来执行native方法;但需要注意的是JVM只是在概念上区分了这两种栈,而并没有规定如何实现,在HotSpot中,则是将JVM栈与本地方法栈二合一,使用核心线程的用户栈来实现(因为JVM栈和本地方法栈都是属于用户态的栈),即Java方法与native方法都在同一个用户栈中调用,而当发生系统调用时,再切换到核心栈运行。

pthread_create : __pthread_create_2_1

int __pthread_create_2_1 (pthread_t *newthread, const pthread_attr_t *attr,
            void *(*start_routine) (void *), void *arg)
{
  STACK_VARIABLES;
  struct pthread *pd = NULL;
  
  // 上游传递的线程属性
  const struct pthread_attr *iattr = (struct pthread_attr *) attr;
  // 为线程分配CPU资源
  cpuset = malloc (cpusetsize);
  // 为线程分配栈资源,分配失败抛出OOM异常-ENOMEM
  int err = ALLOCATE_STACK (iattr, &pd);
  if (__glibc_unlikely (err != 0)) {
      retval = err == ENOMEM ? EAGAIN : err;
      goto out;
  }
  
  // 后续主要为通过当前核心级线程的TCB clone 出一个新的核心级线程的TCB(包括内核栈与用户栈), 这里不再赘述

  return retval;
}

thread_native_entry

OS内核级线程创建完成后,会执行thread_native_entry这个线程入口函数进行OS内核级线程的初始化,并开始运行JavaThread:

static void *thread_native_entry(Thread *thread) {
  OSThread* osthread = thread->osthread();
  // 此时运行的是通过glibc创建的linux核心级线程,所以当前的线程id为linux线程的id
  osthread->set_thread_id(os::current_thread_id());

  {
    // OS核心级线程初始化完毕
    osthread->set_state(INITIALIZED);
  }

  // 初始化完成JVM对应的操作系统线程后,开始运行JavaThread::run
  thread->run();

  return 0;
}

从OS层开始向上传递

OS层 -> JVM层 (通过调用 JavaThread::run 方法)

// The first routine called by a new Java thread
void JavaThread::run() {
  // 初始化TLAB 线程缓冲区(-XX:+UseTLAB)
  this->initialize_tlab();

  // 设置栈、确定栈的生长方向
  this->record_base_of_stack_pointer();
  this->record_stack_base_and_size();

  thread_main_inner();
}

void JavaThread::thread_main_inner() {
    // JavaThread对象中传入的entry_point为Thread对象的Thread::run方法
    this->entry_point()(this, this);
  }
}

JVM层 -> Java语言层 (通过调用Thread::run方法)

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  Handle obj(THREAD, thread->threadObj());
  JavaValue result(T_VOID);
  // JavaCalls模块是用来调用Java方法的
  // 这里其实就是开始调用Java线程对象的run()方法
  JavaCalls::call_virtual(&result,
                          obj, // Thread对象
                          SystemDictionary::Thread_klass(), // Java_java_lang_Thread
                          vmSymbols::run_method_name(), // run()
                          vmSymbols::void_method_signature(), // V
                          THREAD);
}

总结:

至此,我们分析完了一个线程从Java语态到JVM语态再到OS语态的全过程; 可以看到,对于HotSpot VM而言,其在Linux操作系统上实现的Java线程即对OS内核级线程进行了一对一的映射,所有的线程调度、内存分配都交由操作系统进行管理。

作者:CVNot
链接:https://juejin.cn/post/7059363106857680933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值