Hi,今天小护士要准备讲讲Thread
这个核心类了,它在lang包下面,是Java并发编程的基石,重要程度远远大于花哨的LockSupport
、ReentrantReadWriteLock
等等。这个类有大量的native方法,小护士也会带着大家越过JNI,并走到thread.hpp文件看个究竟。
Thread
只是JVM线程的冰山一角,在JVM内核代码中,还有其他专职的线程,例如GC线程。在面试中,如果题目稍微深一点的话,面试官都会要求候选人(来面试的人)大概讲讲线程的初始化过程,以此考察他是否真的了解过那块知识,同时从侧面考察他是否了解JVM与操作系统之间的交互细节。
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
默认为空,栈空间大小默认为零。
public synchronized void start() {
...
if (threadStatus != 0)
throw new IllegalThreadStateException();
...
boolean started = false;
try {
start0();
started = true;
} finally {
...
}
}
经常有道面试题会问,一个线程对象调用了两次start()
会怎么办?那当然是会抛出IllegalThreadStateException
异常。但是,这不是重点,重点是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代码本身。
现在小护士只需要关注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内核代码。
new Thread()
不会真正创建操作系统级别的线程。只会创建一个空壳。JavaThread
线程类型对应Java代码的Thread
,但还有其他JVM线程类型。Thread
对象调用了start()
方法才会真正创建JavaThread
对象进而创建OSThread
对象。OSThread
对象是操作系统级别的线程对象,根据不同操作系统平台有不同的实现。OSThread
对象之前先确认线程需要的栈空间大小。pthread_create()
函数才是创建线程的最终一步。Thread的源码阅读就这么完结了么,No,还有很多细节需要注意,受限于时间和篇幅,这里就不展开讲start()
的技术细节,例如栈空间是如何划分的(red、yellow、reserved、shadow),Linux平台实现与Windows平台实现的差异;也不展开讲其他方法,譬如stop()
、interrupt()
。如果后面还有时间的话,小护士会继续讲解这些细节。今天就到这里,下篇是讲ThreadLocal
源码阅读,别走开哦。