一、启动线程的正确姿势
什么是启动线程的正确姿势呢?
其实说到这里,我在上一篇(一)实现多线程的正确姿势的文章中分析过,启动线程正确的姿势当然是start()
,而不是run()
;
虽然在上一篇中已经对run()
进行了详细的分析,但还没有对start()
进行一个详细的介绍和分析,也没有将start()
和run()
进行一个比较;
/**
* 描述:对比start和run两种启动线程的方式
*
* @author Juning
* @date 2019/10/4 10:12
*/
public class StartAndRunMethod {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName());
};
runnable.run();
new Thread(runnable).start();
}
}
从上面的代码可以看到我们实现了两种方法:
- 先新建一个
Runnable
实例,然后打印出当前线程的名字,创建好之后直接调用它的run()
- 接下来直接
new Thread
,传入我们刚才新建的Runnable
实例,运行Thread
的start()
代码运行结果如下:
根据以上结果我们可以看出两个方法的差异:
run()
打印的结果是main
,也就是由主线程去执行的,这个显然不符合我们的本意,因为我们是想新建一个线程,并且将它启动起来;start()
打印的结果是Thread-0
,也就是我们非常熟悉的,也是用子线程去执行,也和我们的预想一致;
所以这也是为什么启动线程正确的姿势当然是start()
,而不是run()
二、start()
方法原理解读
方法含义
- 启动新线程 启动新线程实际上是有一系列操作的:
- 线程对象在初始化之后调用了
start()
- 当前线程(主线程)去执行
start()
,通知JVM在有空闲的时候执行新线程(子线程)
注: 所以说,启动一个线程的本质是去请求
JVM
来运行它。至于这个线程什么时候去运行,这并不是由我们可以决定的,而是由线程调度器决定的; 如果线程调度器它很忙,或许我们已经调用了start()
,但是它并不一定能够很快的启动; 所以说start()
调用结束后,并不意味着这个线程已经运行了,它可能稍后才会运行,也有可能很长时间都不会被运行,比如说遇到了饥饿的情况; 这也是一个很重要的点,因为很多人都觉得我调用了start()
,它自然而然的就去运行了; 其实不是的,这也是应征了我们在有些情况下线程1
先调用start()
,而线程2
后调用start()
,却发现线程2
先执行,线程1
后执行,这种好像是违背我们线程启动顺序的情况,实际上在学习到这里的时候就明白了: 调用start()
的先后顺序并不能决定线程执行的先后顺序;还有一个重点: 关于
start()
,它其实会让两个线程同时运行
- “主线程”
- 刚刚被创建的子线程
因为我们必须有一个主线程,或者是其他的线程(哪怕不是主线程)来执行
start()
; 很多时候我们都会忽略掉为我们创建线程的主线程,我们不要误以为newThread().start()
就已经是子线程去执行的,这行语句其实是主/父线程去执行的; 这行语句在被主/父线程执行之后,才去创建了我们的子线程;
- 准备工作
上面我们说到
start()
会牵扯到两个线程,其实主/父线程启动的子线程是需要一些准备工作才能运行的:
- 子线程会让自己处于就绪状态
- 等待获取
CPU
资源
就绪状态指的是“我”已经获取到除
CPU
以外的其它资源,比如说我已经设置了上下文、栈、线程状态以及PC
(这里的PC
指的是一个寄存器,指向了程序运行的位置); 做完准备状态之后我们就万事具备,只欠东风了,这个东风就是CPU
资源;在做完准备状态之后,才能被JVM
或者操作系统调度到执行状态,调度到执行状态等待获取CPU
资源,然后才会真正的进度到运行状态,执行我们run()
里面的代码;
- 不能重复
start()
讲到这里我们需要用代码来演示一下:
/**
* 描述:不能两次执行start()
*
* @author Juning
* @date 2019/10/13 16:58
*/
public class CantStartTwice {
public static void main(String[] args) {
Thread thread = new Thread();
thread.start();
thread.start();
}
}
运行结果如下:
可以看到它抛出了一个IllegalThreadStateException
异常,意思是非法的线程状态,也就是说线程状态不符合规定,那么这个原理是什么呢?为什么会抛出这样的异常呢?
其实线程一旦开始执行,它的状态就会从最开始的new
状态进入到后续的状态;
一旦线程执行完毕,它就会进入到终止状态,而终止状态则永远不可能返回回去,所以才会抛出刚才的异常;
这样说可能不太清晰,我们去看看源码!
源码解析
在上面我经常说到线程的状态,线程状态对线程而言是一个非常重要的属性,但这个我会在后续的文章中慢慢的将它们消化掉。
下面我们现在看看start()
的内部:
可以看到,之前为什我们在调用两次start()
的时候会出现异常,图中圈出来的地方就是罪魁祸首!
如果线程状态不等于0(threadStatus != 0
),就抛出IllegalThreadStateException
异常。
那这个threadStatus != 0
又是什么?
根据IDEA代码导航我们可以看到,threadStatus
在它初始化的时候就是0。
/* Java thread status for tools,
* initialized to indicate thread 'not yet started'
*/
我们也可以从变量上面的注释可以看出线程初始的时候被表示为0,也就是还没有启动,所以在执行start()
的时候就要去检查这个线程的状态,看看它是否还处于它被初始化还没有启动的状态,如果不是,那就说明一定有问题!
状态检查完毕之后就会将这个线程加入到一个线程组:
group.add(this);
加入到线程组之后呢就会执行最重要的一个方法start0();,我们接着追踪进去看看:
private native void start0();
到了这一步,我们就可以看到start0()
是一个native
方法,native
代表它的代码不是由Java
实现的,而是由C/C++
去实现的,而这些代码在我们的JDK
里面的具体实现方法也会随着JDK版本的更新发生一些变化。
再使用代码追踪去看看里面的方法的时候我们会发现它已经不能追踪下去了,也就意味着我们不能去直接去看它的具体实现,如果想看这一部分源码,我们可以去openjdk官网找到start0()
的C/C++
的具体实现方法。
在此,我们也不去深究C/C++
的代码了,因为对它不熟,而C/C++
的代码在阅读起来的难度也比Java
高的多。
讲到这里,我们的start()
的原理我们可以总结到:
start
⽅法的执⾏流程是什么?
- 检查线程状态,只有
NEW
状态下的线程才能继续,否则会抛出IllegalThreadStateException
(在运行中或者已结束的线程,都不能再次启)- 被加入线程组
- 调用
start0()
方法启动线程 注意点:start
方法是被synchronized
修饰的方法,可以保证线程安全; 由JVM
创建的main
方法线程和system
组线程,并不会通过start()
来启动。
三、run()
方法原理解读
下面我们再接着来分析分析run()
,也就是让我们记忆犹新的这三行代码:
/**
* If this thread was constructed using a separate
* <code>Runnable</code> run object, then that
* <code>Runnable</code> object's <code>run</code> method is called;
* otherwise, this method does nothing and returns.
* <p>
* Subclasses of <code>Thread</code> should override this method.
*
* @see #start()
* @see #stop()
* @see #Thread(ThreadGroup, Runnable, String)
*/
@Override
public void run() {
if (target != null) {
target.run();
}
}
这三行代码也会诱发两种情况:
- 重写
Thread
类的run()
; - 实现
Runnable
接口并传入Thread类;
而我们刚才直接去执行Runnable
的run()
的时候它就是一个普通的方法,和我们自己写的方法没有什么区别,它的执行线程也是主线程。
所以说要想真正的启动线程,不能直接调用run()
,而是要调用start()
,来间接的去调用run()
。