我们知道再创建线程的时候是使用Thread类中的start方法,那么为什么不直接用Thread类中的run方法呢?
来直接上源码:
- Thread中的run方法如下。
那么target是什么呢?
我们可以看到是一个Runable对象。那么Thread类中的run方法就是说:如果有Runnable子类对象(当然Runable是接口,没有直接对象),就会调用run方法,如果没有该对象,则什么操作都没有!
这是官方文档对该方法的解释:大致意思如上,还多了一条就是Thread类的子类应该覆写此方法。那么为什么要覆写run方法?
我们创建线程有三种方法(具体详细看线程这篇文章),而run方法中规定了线程对象要执行的代码。(也是就是线程要执行的任务操作,通俗的讲就是线程对象要干的事)
通过上边,我们知道如果直接调用Thread类中的run方法,不覆写该方法的话,什么结果都没有,因为Thread类中的run方法虽然实现了Runable接口,但是本身还是一个空方法,这和Runable接口中没有方法体的run方法没有什么区别。(那么为什么Thread要implems实现Runable?)
2.那么既然该方法是空的,那么我们覆写下该方法不就可以通过Threade.run()来实现创建线程了吗?
我们来通过下面代码来测试下:
首先自定义一个线程类来继承Thread,因为只有继承了父类,才能覆写父类方法。
//自定义线程类
class MyThread extends Thread{
//在类中指定线程对象要执行的功能代码
//重写run方法
@Override
public void run() {
while(true){
System.out.println(this.getName());
}
}
}
MyThread线程也要执行的任务(也就是要干的事)是打印执行本方法的线程名字。
主函数中创建我们的线程。并将我们的线程名命名为:“我的第一个线程”,然后调用我们覆写之后的run方法,并在主函数中打印主线程的名字。来看结果:我们预料的结果应该是main线程和“我的第一个线程”抢占CUP内存,交替打印到控制台。
public static void main(String[] args) {
MyThread thread=new MyThread();
thread.setName("我的第一个线程");
thread.run();
while(true){
System.out.println(Thread.currentThread().getName());
}
}
我们发现根本就没有。因为我们的“第一个线程是一个死循环”根本都执行不到main线程,所以没法打印出main线程名。
因为这根本就没有新创建一个线程,而是把我们的run方法加载到主方法来执行了而已,就和我们平常的方法调用没有什么区别。真正开启新线程的方法是start,我们来看下start方法
译文使该线程开始执行;Java虚拟机调用这个线程的run方法。 结果是两个线程并发运行:当前线程(从对start方法的调用中返回)和另一个线程(执行其run方法)。 多次启动一个线程是不合法的。特别是,线程在完成执行后可能不会重新启动。
大致意思是从这里出现了两个线程,一个是我们覆写的run线程一个调用start线程的main线程。
那么到这里我们已经解释了为什么不能直接run来实现创建线程。
那既然不能用run方法直接创建线程,需要使用start方法,而创建的新线程中需要执行的操作(也就是要干的事)是放到覆写之后的run方法中的,那我们的大概思路已经清晰了。
start方法启动线程来调用run方法中新建线程要执行的操作。
也就是本文的主题,那么start方法是如何调用run方法的。
上源码:
我们看start方法的核心应该是group.add方法或者start(0)方法,我们打开group.add,发现是将线程保存到数组中,但是没有发现run的痕迹,于是我们再来看start(0)
native修饰的一个私有方法,那么被native修饰有什么意义呢?
native:native是一个计算机函数,一个Native Method就是一个Java调用非Java代码的接口。方法的实现由非Java语言实现,比如C或C++。
Java平台有个用户和本地C代码进行互操作的API,称为Java Native Interface (Java本地接口)。我们知道了这个方法是java调用底层c的接口。该过程由JVM底层来完成。
那么Java是如何通过JNI和c语言通信的呢?
通过RegisterNatives方法,我们来看Thread的该方法.
Make sure registerNatives is the first thing <clinit> does.
确保registernative是客户端所做的第一件事。static代码块修饰的方法,再方法区类加载到JVM的时候就被加载,该方法可以调用JNI接口。注册一些本地方法供 Thread 类使用,如 start0(),stop0() 等等,可以说,==所有操作本地线程的本地方法都是由它注册的 ==. 这个方法放在一个 static 语句块中,这就表明,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地方法
至此,大致线程大致就是start()–>JVM–>run()
我们继续往下看,registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,定义了各个操作系统平台都要用到的关于线程的公用数据和操作。
JNIEXPORT void JNICALL
Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){
(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}
static JNINativeMethod methods[] = {
{"start0", "()V",(void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive","()Z",(void *)&JVM_IsThreadAlive},
{"suspend0","()V",(void *)&JVM_SuspendThread},
{"resume0","()V",(void *)&JVM_ResumeThread},
{"setPriority0","(I)V",(void *)&JVM_SetThreadPriority},
{"yield", "()V",(void *)&JVM_Yield},
{"sleep","(J)V",(void *)&JVM_Sleep},
{"currentThread","()" THD,(void *)&JVM_CurrentThread},
{"countStackFrames","()I",(void *)&JVM_CountStackFrames},
{"interrupt0","()V",(void *)&JVM_Interrupt},
{"isInterrupted","(Z)Z",(void *)&JVM_IsInterrupted},
{"holdsLock","(" OBJ ")Z",(void *)&JVM_HoldsLock},
{"getThreads","()[" THD,(void *)&JVM_GetAllThreads},
{"dumpThreads","([" THD ")[[" STE, (void *)&JVM_DumpThreads},
};
我们找到了start(0),Java 线程调用 start 方法,实际上会调用到 JVM_StartThread 方法,那这个方法又是怎样的逻辑呢?
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
…
native_thread = new JavaThread(&thread_entry, sz);
…
这里JVM_ENTRY是一个宏,用来定义JVM_StartThread 函数,可以看到函数内创建了真正的平台相关的本地线程,其线程函数是 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,
KlassHandle(THREAD,SystemDictionary::Thread_klass()),
vmSymbolHandles::run_method_name(),
vmSymbolHandles::void_method_signature(),THREAD);
}
可以看到调用了 vmSymbolHandles::run_method_name 方法,这是在 vmSymbols.hpp 用宏定义的:
class vmSymbolHandles: AllStatic {
…
template(run_method_name,"run")
…
}
至此完善一下整个过程如下:
完毕!