小护士青铜上分系列之《Java源码阅读》第三篇Thread

小护士青铜上分系列之《Java源码阅读》第三篇Thread

Hi,今天小护士要准备讲讲Thread这个核心类了,它在lang包下面,是Java并发编程的基石,重要程度远远大于花哨的LockSupportReentrantReadWriteLock等等。这个类有大量的native方法,小护士也会带着大家越过JNI,并走到thread.hpp文件看个究竟。

Thread只是JVM线程的冰山一角,在JVM内核代码中,还有其他专职的线程,例如GC线程。在面试中,如果题目稍微深一点的话,面试官都会要求候选人(来面试的人)大概讲讲线程的初始化过程,以此考察他是否真的了解过那块知识,同时从侧面考察他是否了解JVM与操作系统之间的交互细节。

1. Thread init()

Thread实现了Runnable接口,它有很多奇怪的字段,但只有一部分是在初始化的时候用到。new Thread()调用了init()方法。

小护士有个坏习惯,如果看到大段代码,眼镜会感到不适,所以忽略init()方法细节,来个简化版的大概讲讲:

private volatile char  name[];               // 指定线程名称
private int            priority;             // 默认继承当前线程
private boolean        daemon = false;       // 默认不是守护线程
private boolean        stillborn = false;    // 默认JVM状态
private Runnable       target;               // new Thread(()->{})用到
private ThreadGroup    group;                // 默认取SecurityManager的,若为空则继承当前线程的
private ClassLoader    contextClassLoader;   // 默认继承当前线程
private long           tid;                  // 全局自增的,但不是操作系统线程号
private long           stackSize;            // 线程执行所需的栈空间大小,默认为0

这里小护士有意地忽略一些小众字段,因为不是重点啊。重点是Thread的那些native方法。其实,这里只需要记住几点就好了,线程ID是自增的,名称是可以指定的,也可以用默认生成的,优先级默认继承当前线程的,线程默认不是守护线程,Runnable target默认为空,栈空间大小默认为零。

2. Thread start()

public synchronized void start() {
...
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
...
    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
    ...
    }
}

经常有道面试题会问,一个线程对象调用了两次start()会怎么办?那当然是会抛出IllegalThreadStateException异常。但是,这不是重点,重点是start0()干了什么。

3. Thread start0()

小护士又要开始跟踪底层代码了,嘿嘿。
1. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/java.base/share/native/libjava/Thread.c
先从常规的native包下找libjava/Thread.c,找到下面的代码细节:

...
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
...
};
... 

2. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/prims/jvm.cpp
然后来到这个官方JNI核心文件,jvm.cpp。里面有很多JDK的native方法具体实现。现在,只需找到JVM_StartThread方法定义即可。

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;

   ... ignore if-else block
   {
      jlong size =
             java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));

      NOT_LP64(if (size > SIZE_MAX) size = SIZE_MAX;)
      size_t sz = size > 0 ? (size_t) size : 0;
      native_thread = new JavaThread(&thread_entry, sz);

      if (native_thread->osthread() != NULL) {
        // Note: the current thread is not being used within "prepare".
        native_thread->prepare(jthread);
      }
  }
  ...
  Thread::start(native_thread);

JVM_END

这里面有两行代码比较关键。

  • native_thread = new JavaThread(&thread_entry, sz);
  • Thread::start(native_thread);

需要注意的是,JavaThread在初始化的时候就要确定栈空间大小。而栈空间的具体定义在thread.hpp有提及到。下面先看JavaThread构造函数。

3. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/thread.hpp

按照惯例,先看头文件声明内容,发现了线程分类的秘密。Java线程除了在Java代码自己new Thread()的线程属于JavaThread类型以外,还有其他几种Thread。这些Thread都将会在小护士不久将来的《JVM源码阅读》中讲解到,现在是《Java8源码阅读》,主要内容还是围绕Java代码本身。

  • Class hierarchy
    • Thread
      • NamedThread
        • VMThread
        • ConcurrentGCThread
        • WorkerThread
          • GangWorker
          • GCTaskThread
      • JavaThread
        • CompilerThread
        • ServiceThread
        • CodeCacheSweeperThread
      • WatcherThread

现在小护士只需要关注JavaThread的构造函数的细节。

4. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/share/runtime/thread.cpp

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread() {
  initialize();
  _jni_attach_state = _not_attaching_via_jni;
  set_entry_point(entry_point);
  // Create the native thread itself.
  // %note runtime_23
  os::ThreadType thr_type = os::java_thread;
  thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread : os::java_thread;
  os::create_thread(this, thr_type, stack_sz);
}

JavaThread的构造函数的参数entry_point是一个函数指针,指向jvm.cpp中的静态函数thread_entry()。这个函数根据注释的描述,大概意思就是防止线程对象在被其他线程操作时退出。

static void thread_entry(JavaThread* thread, TRAPS) {
  ... ignore function detail
}

第二个参数不用多说,就是栈空间大小。在构造函数中,有两行关键的代码:

initialize();                                  // 初始化
os::create_thread(this, thr_type, stack_sz);   // 创建`osThread`

initialize()函数主要是做了很多赋值操作,置空置零,最后调用了操作系统级别的初始化函数pd_initialize(),这个方法会根据操作系统平台编译不同版本。pd的意思就是platform dependent。

os::create_thread()函数也是跟操作系统平台直接相关,顾名思义就是创建操作系统级别的线程。

小护士决定以linux平台为例继续往下跟踪create_thread()

5. https://github.com/unofficial-openjdk/openjdk/blob/jdk/jdk/src/hotspot/os/linux/os_linux.cpp

bool os::create_thread(Thread* thread, ThreadType thr_type, size_t req_stack_size) {
  ...
  OSThread* osthread = new OSThread(NULL, NULL);
  ...
  osthread->set_thread_type(thr_type);
  osthread->set_state(ALLOCATED);
  thread->set_osthread(osthread);
  ...
  size_t stack_size = os::Posix::get_initial_stack_size(thr_type, req_stack_size);
  ...
  int status = pthread_attr_setstacksize(&attr, stack_size);
  ...
  {
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
    ...
  }
  ...
  return true;
}

create_thread()函数逻辑其实并不复杂,关键代码行就是int ret = pthread_create(...),这是一个POSIX编程接口。有个细节需要注意的是,JavaThread对象引用了OSThread对象,在OSThread对象创建之前,要先确认栈空间大小stack_size

POSIX是操作系统标准API,所有上层应用想要与操作系统打交道都要通过POSIX提供的编程接口实现内存管理、进程管理、驱动程序调用等等。

还需要往下了解pthread_create()吗?不需要了,再往下就进入kernel层面了,小护士暂时没有能力阅读Linux内核代码。

4. Thread 小结

  • Java代码的new Thread()不会真正创建操作系统级别的线程。只会创建一个空壳。
  • JVM的JavaThread线程类型对应Java代码的Thread,但还有其他JVM线程类型。
  • 只有Java代码的Thread对象调用了start()方法才会真正创建JavaThread对象进而创建OSThread对象。
  • OSThread对象是操作系统级别的线程对象,根据不同操作系统平台有不同的实现。
  • 在Linux平台上,创建OSThread对象之前先确认线程需要的栈空间大小。
  • POSIX的pthread_create()函数才是创建线程的最终一步。

Thread的源码阅读就这么完结了么,No,还有很多细节需要注意,受限于时间和篇幅,这里就不展开讲start()的技术细节,例如栈空间是如何划分的(red、yellow、reserved、shadow),Linux平台实现与Windows平台实现的差异;也不展开讲其他方法,譬如stop()interrupt()。如果后面还有时间的话,小护士会继续讲解这些细节。今天就到这里,下篇是讲ThreadLocal源码阅读,别走开哦。

你可能感兴趣的:(青铜上分,Java源码)