Thread.start()之后是如何调用run()方法的呢?
前言
我们在初学 Java
中的线程的时候,可能会写过如下代码:
public static void main(String[] args) {
Thread thread = new Thread(() ->
System.out.println("当前线程名称:" + Thread.currentThread().getName())
, "jts-thead-1");
thread.start();
}
等价于:
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("当前线程名称:" + Thread.currentThread().getName());
}
}, "jts-thead-1");
thread.start();
}
打印结果如下:
当前线程名称:jts-thead-1
那么你有没有思考过,为什么我调用了Thread.start()
方法,会打印出这段话呢? 你可以先思考下,下面即将开始探索之旅。
1.java 层面代码
public synchronized void 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) {
}
}
}
private native void start0();
可以发现,这是一个 native
方法,我们想要研究native
方法,就要跟踪到hotspot
源码里面去了。
2.探索hotspot
源码
2.1 环境准备
我这里下载的是openjdk8
的源码,用的工具是clion
,都是jetbrains
旗下软件,使用起来和idea
是一样的。吐槽下:构建openjdk
源码是个艰难的过程,搭建过程中我也遇到了很多意想不到的坑,基本都是环境问题。不过周志明老师在《凤凰架构》里面有个 OpenJDK with CLion 懒人包
,你只需在自己机器上安装下docker
,然后按照上面教程操作即可。需要注意的是他这个里面的源码是基于JDK15
的,如果你想用不用的版本,在脚本里面进行地址替换即可我这里使用的是JDK8
,我看了下JDK8
中不同的小版本也有些差异,但是大体上的代码是一致的。
2.2 查找规则
Hotspot
源码基本都是c
和c++
写的,上面我们看到Java
启动线程的时候,最终调用的是start0()
方法,那么start0()
方法是如何映射到Hotspot
代码中呢?
如果我们不知道Java
中的native
方法在Hotspot
的对应规则的话,可以在Hotspot
源码里面全局搜下start0()
,结果如下:
这个是在Thread.c
的文件里面。可以看到它在这里面维护了一个数组,来映射Java
中的native
方法和Hotspot
的方法。
这里也可以看到Thread.java
对应了这里Thread.c
,你也可以类比下其他的文件有没有这样的规律。
由于方法内容较多,所以下面的代码中我会删减掉次要的代码,只保留主流程的代码,这样能看起来更清晰。
因为我们要看的是start0()
方法,就点到这个JVM_StartThread()
方法中查看,下面我们就踏上真正的探索之旅。
2.3 探索之旅
JVM_StartThread
方法是在jvm.cpp
文件中:
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JavaThread *native_thread = NULL;
{
// 创建native线程 注意:&thread_entry 也是一个方法。
native_thread = new JavaThread(&thread_entry, sz);
if (native_thread->osthread() != NULL) {
// 对线程做一些准备工作
native_thread->prepare(jthread);
}
}
}
Thread::start(native_thread);
这里调用了JavaThread
的构造方法,构造方法里面传的有个参数thread_entry
需要重点注意一下,这个参数其实是个方法,我们可以点击进去看下:
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,
obj,
vmClasses::Thread_klass(),
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);
}
我们需要注意call_virtual
方法中的几个参数:
obj
:就是当前线程对象vmClasses::Thread_klass()
:表示调用的对象的类型是java.lang.Thread
vmSymbols::run_method_name()
:表示调用的方法是run
vmSymbols::void_method_signature()
: 表示返回结果是空。
映射关系如下:
vmClassMacros.hpp 中 do_klass(Thread_klass,java_lang_Thread)
vmSymbols.hpp 中 template(run_method_name,“run”)
划重点:请务必记住注册的这个方法,考试要考的。
然后跟踪到JavaThread()
的构造方法中去:
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : JavaThread() {
// 设置回调函数,刚才划重点的函数
set_entry_point(entry_point);
// 创建线程
os::create_thread(this, thr_type, stack_sz);
}
这样就向线程中注册了回调函数。
接下来我们看create_thread()
这个创建线程的方法。这个方法是在os.hpp
中定义的,由于Java
是跨平台的,在不同的操作系统上会有不同的实现。我们可以认为os.hpp
是一个接口,而os_linux.cpp
、os_windows.cpp
等不同操作系统对应的文件是它的实现类。我这里主要是看linux
平台对应的代码,接下的代码展示都是linux
平台对应的代码。
bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
}
依然可以看到,这里面调用了 thread_native_entry
方法,
static void *thread_native_entry(Thread *thread) {
{
// notify parent thread
osthread->set_state(INITIALIZED);
sync->notify_all();
// wait until os::start_thread()
while (osthread->get_state() == INITIALIZED) {
sync->wait_without_safepoint_check();
}
}
// 回调run
thread->call_run();
return 0;
}
可以看到这里是将当前当前线程wait
,那么什么时候会被唤醒呢?直到调用了start_thread()
方法的时候。唤醒之后做什么呢?就是回调run()
,也就是回调我们刚才向线程中注册的方法。
我们先看下thread.cpp
的call_run()
方法,揭晓最终答案,然后再看这里的线程被wait之后,什么时候被notify的。
void Thread::call_run() {
// Invoke <ChildClass>::run()
this->run();
}
继续跟踪run()
方法:
void JavaThread::run() {
// We call another function to do the rest so we are sure that the stack addresses used
// from there will be lower than the stack base just computed.
thread_main_inner();
}
继续跟踪thread_main_inner()
void JavaThread::thread_main_inner() {
this->entry_point()(this, this);
}
这里就会调用entry_point()
方法,就是我们刚才set_entry_point(entry_point)的时候设置进去的画重点的回调函数!也就是说在这里就会调用 java.lang.Thread.run()方法了!
下面我们继续看下,创建线程之后,将线程wait
之后,是在哪里notify
的。
注释里说是调用了os::start_thread()
之后notify
的,接下来我们只需要跟下去验证下就好了。
我们从上面的 Thread::start(native_thread)
方法继续往下跟,
此方法在Thread.cpp
文件中:
void Thread::start(Thread* thread) {
if (thread->is_Java_thread()) {
java_lang_Thread::set_thread_status(JavaThread::cast(thread)->threadObj(),
JavaThreadStatus::RUNNABLE);
}
os::start_thread(thread);
}
点击去
os::start_thread(thread);
方法继续跟踪,此方法是在os.cpp
中:
void os::start_thread(Thread* thread) {
OSThread* osthread = thread->osthread();
osthread->set_state(RUNNABLE);
pd_start_thread(thread);
}
在os_linux.cpp
中pd_start_thread(thread)
:
void os::pd_start_thread(Thread* thread) {
sync_with_child->notify();
}
注释果然诚不欺我。果然是在os::start_thread(thread)
中 notify
了。
3.总结
总结下就是Thread.start()
将线程启动后,Thread.run()
方法将会被JVM
进行回调。
其实这个答案已经在 Thread.java
类的注释中已经说了:
哈哈,无聊的知识又增加了。
不过通过这个知识点,又回顾了一下大学学的C
和C++
,然后也了解了一些线程启动的一些细节,还顺带看了下线程的生命周期相关的代码,还是很有收获的。
小思考题:
我们可以看到 Thread.run()
方法返回的是 void
,如果我们想拿到结果的返回值,让你自己实现的话,你会怎么做呢?
好了,这篇文章就到这里了,感谢大家的观看!欢迎大家关注我的公众号《贾哇技术指南》。