跳到正文
hello world

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()

4. start() 和 run() 的区别 - Java并发之从Thread到CompletableFuture - 上下文网