Java线程是如何启动的?

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()(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.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,如果我们想拿到结果的返回值,让你自己实现的话,你会怎么做呢?

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

Java程是Java中的一种机制,用于实现并发编程。线程可以看作是程序执行流的最小单元,能够独立运行并执行任务。Java线程是通过java.lang.Thread类来实现的,可以继承Thread类或实现Runnable接口来创建线程Java线程的运用可以通过以下几个步骤来实现: 1. 创建线程对象 在Java中创建线程对象有两种方式,一种是继承Thread类,另一种是实现Runnable接口。继承Thread类需要重写run()方法,实现Runnable接口需要实现run()方法。 2. 启动线程 创建线程对象后,需要调用线程对象的start()方法启动线程调用start()方法后,线程进入就绪状态,等待系统调度执行。 3. 线程执行任务 线程启动后,自动执行run()方法中的任务。在任务执行过程中,可以通过sleep()方法、yield()方法等来控制线程的执行。 4. 线程结束 线程执行完任务后,自动退出。在多线程编程中,需要注意线程的结束状态,避免出现线程泄漏或死锁等问题。 以下是一个简单的Java线程示例代码,通过继承Thread类来创建线程: ```java public class SimpleThread extends Thread { public void run() { System.out.println("线程开始执行!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行完毕!"); } public static void main(String[] args) { SimpleThread thread = new SimpleThread(); thread.start(); System.out.println("主线程执行完毕!"); } } ``` 在上述代码中,我们创建了一个继承自Thread类的SimpleThread类,并在run()方法中定义了线程要执行的任务。在main()方法中,我们创建了一个SimpleThread对象,并调用它的start()方法启动线程。在start()方法调用后,线程自动调用run()方法来执行任务。 除了继承Thread类外,我们还可以通过实现Runnable接口来创建线程。以下是一个实现Runnable接口的示例代码: ```java public class SimpleRunnable implements Runnable { public void run() { System.out.println("线程开始执行!"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("线程执行完毕!"); } public static void main(String[] args) { SimpleRunnable runnable = new SimpleRunnable(); Thread thread = new Thread(runnable); thread.start(); System.out.println("主线程执行完毕!"); } } ``` 在上述代码中,我们创建了一个实现了Runnable接口的SimpleRunnable类,并在run()方法中定义了线程要执行的任务。在main()方法中,我们创建了一个SimpleRunnable对象,并将它作为参数传递给Thread类的构造方法来创建一个新的线程。最后,我们调用线程的start()方法启动线程。 以上就是Java线程的简单运用,当然Java线程的使用还涉及到线程同步、线程池等高级特性,需要进一步学习和实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值