Loading...
墨滴

贾哇技术指南

2021/09/26  阅读:24  主题:默认主题

Thread.start()之后是如何调用run()方法的?

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源码基本都是cc++写的,上面我们看到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.cppos_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.cppcall_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()(thisthis);
}

这里就会调用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.cpppd_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类的注释中已经说了:

哈哈,无聊的知识又增加了。 不过通过这个知识点,又回顾了一下大学学的CC++,然后也了解了一些线程启动的一些细节,还顺带看了下线程的生命周期相关的代码,还是很有收获的。

小思考题: 我们可以看到 Thread.run() 方法返回的是 void,如果我们想拿到结果的返回值,让你自己实现的话,你会怎么做呢?

好了,这篇文章就到这里了,感谢大家的观看!欢迎大家关注我的公众号《贾哇技术指南》。

贾哇技术指南

2021/09/26  阅读:24  主题:默认主题

作者介绍

贾哇技术指南