4. start() 和 run() 的区别
发布于 • 阅读量 0
4. start() 和 run() 的区别
上一节已经用 Thread 启动了一个子线程,让它去处理一个 PDF。
这一节单独把 start() 和 run() 拿出来说一下。
这个点看起来很基础,但我觉得必须搞清楚。因为很多刚接触线程的人,看到 Thread 里面有一个 run() 方法,就会下意识觉得:那我直接调用 run() 不就行了吗?
其实不行。
或者更准确地说:能调用,但它不是多线程执行。
先看一段对比代码
新建一个类:
com.succos.thread.ThreadStartRunDemo
代码如下:
package com.succos.thread;
public class ThreadStartRunDemo {
public static void main(String[] args) {
System.out.println("main 方法开始:" + Thread.currentThread().getName());
Thread thread = new Thread(() -> {
System.out.println("任务开始执行:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("任务执行完成:" + Thread.currentThread().getName());
}, "pdf-thread-1");
// 注意:这里先调用 run()
thread.run();
System.out.println("main 方法结束:" + Thread.currentThread().getName());
}
}
这段代码里,我虽然创建了一个 Thread 对象,但调用的是:
thread.run();
不是:
thread.start();
运行以后,控制台大概会输出:
main 方法开始:main
任务开始执行:main
任务执行完成:main
main 方法结束:main
重点看线程名。
任务是在:
main
线程里执行的。
也就是说,虽然代码写在 new Thread(...) 里面,但只要我直接调用 run(),它就只是一个普通方法调用,并没有启动新线程。
run() 本质上只是一个普通方法
Thread 里面确实有 run() 方法。
但是直接调用:
thread.run();
和调用一个普通对象的方法,本质上没什么区别。
它的执行流程是:
main 线程执行 main 方法
↓
main 线程调用 thread.run()
↓
main 线程进入 run 方法里面执行任务
↓
run 方法执行完
↓
main 线程继续往下执行
所以这时只有一条执行路径。
可以理解成:
main 线程自己把 PDF 处理了
没有创建新的执行线程。
这也是为什么刚才日志里打印出来的线程名都是 main。
再把 run() 换成 start()
现在把代码里的:
thread.run();
改成:
thread.start();
完整代码如下:
package com.succos.thread;
public class ThreadStartRunDemo {
public static void main(String[] args) {
System.out.println("main 方法开始:" + Thread.currentThread().getName());
Thread thread = new Thread(() -> {
System.out.println("任务开始执行:" + Thread.currentThread().getName());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("任务执行完成:" + Thread.currentThread().getName());
}, "pdf-thread-1");
// 注意:这里调用 start()
thread.start();
System.out.println("main 方法结束:" + Thread.currentThread().getName());
}
}
这次运行以后,输出可能类似这样:
main 方法开始:main
main 方法结束:main
任务开始执行:pdf-thread-1
任务执行完成:pdf-thread-1
也可能是:
main 方法开始:main
任务开始执行:pdf-thread-1
main 方法结束:main
任务执行完成:pdf-thread-1
顺序不一定完全一样。
但重点是,任务已经不在 main 线程里执行了,而是在:
pdf-thread-1
里执行。
这才是真正启动了一个新线程。
start() 到底做了什么?
我现在可以先不看源码,只从效果上理解。
调用:
thread.start();
之后,Java 会让 JVM 去启动一个新的线程。
这个新线程启动以后,会自动调用这个线程对象的 run() 方法。
也就是说,真正的流程应该是:
main 线程调用 thread.start()
↓
JVM 创建并启动一个新的操作系统线程
↓
新线程开始执行
↓
新线程自动进入 run() 方法
↓
run() 里面的任务逻辑开始执行
所以这里有一个很重要的区别:
run() 是任务入口;
start() 是启动线程。
直接调用 run(),只是普通方法调用。
调用 start(),才是让 JVM 新开一条执行路径。
用 PDF 处理来理解
如果我用 run() 处理 PDF,实际效果是这样:
main 线程开始
↓
main 线程处理 test1.pdf
↓
PDF 处理完成
↓
main 线程结束
它还是单线程。
如果我用 start(),效果是这样:
main 线程开始
↓
main 线程启动 pdf-thread-1
↓
pdf-thread-1 处理 test1.pdf
↓
main 线程继续往下执行
这时才是两个线程在跑。
一个负责主流程,一个负责 PDF 处理任务。
为什么 start() 不能调用两次?
还有一个细节也值得记一下。
同一个 Thread 对象,start() 只能调用一次。
比如这样写:
Thread thread = new Thread(() -> {
System.out.println("执行任务");
});
thread.start();
thread.start();
第二次 start() 会报错:
java.lang.IllegalThreadStateException
原因是一个线程对象一旦启动过,就不能再次启动。
它的生命周期大概是:
新建
↓
就绪
↓
运行
↓
结束
线程结束以后,这个 Thread 对象也不能重新拿来启动。
如果还想再执行一次任务,就要重新创建一个新的 Thread 对象。
比如:
new Thread(task, "pdf-thread-1").start();
new Thread(task, "pdf-thread-2").start();
这一点和线程池不一样。
线程池里的线程是可以复用的,但普通 Thread 对象启动完一次就结束了,不能重复启动。
为什么 run() 可以调用多次?
和 start() 不一样,run() 可以调用多次。
比如:
thread.run();
thread.run();
这不会报 IllegalThreadStateException。
因为它只是普通方法调用。
但是它也不会开启两个线程,只是 main 线程顺序执行了两次 run() 方法。
所以这点也能反过来帮助理解:
start() 是线程生命周期相关的方法;
run() 只是线程要执行的任务内容。
我对这两个方法的理解
我现在会这样区分它们:
run():定义任务怎么执行;
start():真正启动一个新线程去执行这个任务。
或者更直接一点:
想要多线程,就调用 start();
直接调用 run(),还是当前线程自己干活。
这句话虽然简单,但很关键。
后面不管是 Runnable、线程池,还是 CompletableFuture,本质上都会涉及这个问题:
任务到底是在哪个线程里执行的?
只要能看清楚这一点,很多并发代码就不会那么绕。
这一节小结
这一节我主要记住几个点:
1. 直接调用 run(),不会启动新线程;
2. 调用 start(),才会真正启动一个新的线程;
3. start() 内部最终也会让新线程去执行 run();
4. 同一个 Thread 对象只能 start 一次;
5. run() 可以调用多次,但它只是普通方法调用。
如果用一句话总结:
start() 负责开线程,run() 负责放任务。
下一节继续看 join()。
因为现在虽然能启动子线程了,但 main 线程不会主动等它。实际批量处理 PDF 时,我肯定需要知道所有文件什么时候处理完,这就要用到 join()。