导语
在面试中,很多童鞋可能被问到过start方法和run方法的区别,貌似这种问题都被问烂了,笔者貌似也被问过,不记得当初是怎么回答的了,但肯定回答的怎么样。那么对于这样的问题,我们应该怎么回答呢?
当然首先肯定要从理论层面说一下,然后从代码层面说一下,但是理论还是要借助于代码实现来衬托的,所以本篇文章大体思路应该是先从代码实现上比较一下,然后对每个方法的基本实现进行一个简单的阐述。注:本篇文章只基于java代码实现来说,不基于JVM源码。
一、start方法与run方法的比较
对于start方法和run方法的比较,我们还是先从代码上来看,如下:
public class StartAndRunThreadDemo {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
// Thread thread = new Thread(new Runnable() {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName() + " invoked...");
// }
// });
// thread.start();
// thread.run();
}
}
上面代码的执行结果如下:
"C:/Program Files/Java/jdk1.8.0_131/bin/java.exe" ...
main
Thread-0
从执行结果上,我们可以得知runnable.run()方法的执行结果是表示它是main线程所执行的,而start方法是由子线程去执行的,可是run()方法由主线程所执行的,这便不符合多线程环境下所具有的意义了。那我们的本意是什么呢?我们的本意是要新建一个线程然后把它启动起来。其实就是因为run方法它只是一个普通的java方法,它并不具有启动线程的作用。为什么这么说呢?我们来看下官网是如何阐述的:
上面的意思用简单的话来说就是:如果是直接调用Thread类中的run方法,而不不覆写该方法,那将没有上面实质的结果,虽然Thread类中的run方法实现了Runable接口,但是其本身并不具有启动线程的意义,这和Runable接口中的run方法没有什么实质的区别。对此我们后面还将进行说明。
二、start方法的(执行)原理
1.start方法的含义:
(1)线程的启动
首先来说start方法的含义是具有启动线程的作用的,而这里所说的线程便是我们常说的主线程,对此我们依旧以官网的解释为主,如下:
大体意思就是:当先处对象初始化后执行start方法,然后当前线程(主线程)去通知JVM虚拟机,如果虚拟机有时间的的话来执行新的线程,也就是Java虚拟机调用这个线程的run方法,此时的结果便是两个线程同时运行:当前线程(主线程)(从调用返回到start方法)和另一个线程(子线程)(执行其run方法)。
从上面我们知道,启动一个线程的本质就是通知JVM来运行一个线程,对于这个线程何时运行,就需要线程调度器去决定了,这个意思也就是说嗲用start方法的顺序并不能决定线程的运行顺序。
(2)具体准备工作
上面提到了另一个线程,就是子线程,后面统一用子线程来说明,但是子线程的运行时需要一些准备的,譬如汽车起动前需要:加满汽油、加满机油、保证蓄电池电量充足、启动开关没坏等等。线程运行前的准备也是如此,因此首先是需要使其处于就绪状态,就绪状态的意思是获得到了CPU以外的其他资源,譬如设置上下文、栈、线程状态以及寄存器,寄存器指向了程序的运行位置。当昨晚完以上操作后,子线程才能进一步被JVM调度到执行状态,调度到执行状态后,如果获得到了CPU资源,此时才会真正的进入到运行状态。其实这也可以看做是你开着一辆车行驶在马路上,你启动了汽车不代表你就可以跑了,而是要依赖于马路上的红绿灯。红绿灯就相当于JVM的调度作用了。
(3)两次调用start方法的结果
上面的官网截图中说:多次启动一个线程是不合法的。特别是,线程在完成执行后可能不会重新启动。意思就是说不可重复执行start方法。对此就不做代码演示了,感兴趣的童鞋可以自己去试试。但是我们会在下面的源码讲述中进行简单的说明。
2.start源码解析
关于start方法代码的执行可以简单分为以下几个步骤:
- 启动时检查线程状态
- 添加到线程组
- 调用本地方法start0
那么我们还是来看下代码中是如何实现的吧,如下:
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) {
}
}
}
从上面的代码中,可以看出其大体执行步骤的确如我们上面所说的,首先是建厂线程状态,这也就是我们前面所说的重复执行start方法时,便会在此进行校验。然后便是添加线程组,我们也来看一下此步操作的代码:
void add(Thread t) {
synchronized (this) {
if (destroyed) {
throw new IllegalThreadStateException();
}
if (threads == null) {
threads = new Thread[4];
} else if (nthreads == threads.length) {
threads = Arrays.copyOf(threads, nthreads * 2);
}
threads[nthreads] = t;
nthreads++;
nUnstartedThreads--;
}
}
其实此时我们应该考虑这样有个问题,我们只调用start方法,那么我们在哪里运行线程呢?那我们继续看下start0()方法的实现:
private native void start0();
此时你会不会很失望,native方法便是虚拟机内的方法了,前面我们说了本篇文章不涉及JVM源码,所以这里就不做详细的代码展示了。
三、run方法原理
run方法的代码我们前面的文章已经做了一些展示,也就那么几行,这里我们还是贴一下代码吧,如下:
public void run() {
if (target != null) {
target.run();
}
}
其实我们前面也说了为什么我们启动一个线程必须嗲用start方法,而不是run方法,这里从代码中我们也可以看出,run方法就是个普通的java方法,并没有实质性的操作系统的意义,而调用start方法是要调用jvm源码的,在jvm源码中会调用其相应的run方法的。这里就不赘述了。
总结
本篇文章进对run方法和start方法进行了简单的对比,其意义是要体现启动线程的正确姿势。我们文章的大体脉络如下:
- start方法与run方法的比较
- start方法的原理
- run方法的原理
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19406.html