孤舟笔记 并发篇十八 为什么启动线程不能直接调用run()方法?调用两次start()又会怎样?这个设计藏着大智慧

文章目录

    • [一、先说结论:run() 和 start() 的核心区别](#一、先说结论:run() 和 start() 的核心区别)
    • [二、直接调用 run():根本没有新线程](#二、直接调用 run():根本没有新线程)
      • [start() 源码做了什么?](#start() 源码做了什么?)
    • [三、调两次 start():直接报错](#三、调两次 start():直接报错)
    • 四、正确姿势:需要新线程就创建新对象
    • [五、Thread 的状态机:为什么只能 start 一次](#五、Thread 的状态机:为什么只能 start 一次)
    • [run() vs start() 全景](#run() vs start() 全景)
    • 回答技巧与点评

个人网站

新手写多线程,最容易犯两个错:一是直接调 run() 而不是 start(),二是同一个线程对象调两次 start()。前者"线程"根本没启动,后者直接抛异常。但你有没有想过,为什么 Java 要这么设计?

搞懂了这两个问题,你对线程生命周期的理解就跨过了"会用"的门槛。

一、先说结论:run() 和 start() 的核心区别

维度 start() run()
作用 启动新线程,由 JVM 调用 run() 在当前线程中执行 run() 方法体
是否创建新线程 ✅ 是 ❌ 否
调用次数 只能调一次 可以反复调
重复调用 抛 IllegalThreadStateException 正常执行
底层机制 native start0() 创建 OS 线程 普通方法调用

一句话记住:start() 是"开工仪式",run() 是"工作内容"------仪式只能搞一次,但活可以反复干。

二、直接调用 run():根本没有新线程

java 复制代码
Thread t = new Thread(() -> {
    System.out.println("当前线程: " + Thread.currentThread().getName());
});

t.run();   // 输出:当前线程: main 👈 在主线程执行的!
t.start(); // 输出:当前线程: Thread-0 👈 这才是新线程

调 run() 就像: 你叫了一个外卖,但没下单(没 start),自己跑去店里吃了------外卖员(新线程)根本没出动。

调 start() 就像: 你正式下了单,外卖员出发送货------新线程启动了,run() 会在新线程中被 JVM 自动调用。

start() 源码做了什么?

java 复制代码
public synchronized void start() {
    if (threadStatus != 0)  // 状态检查 👈
        throw new IllegalThreadStateException();
    group.add(this);
    start0();  // native 方法,创建 OS 线程 👈
}

private native void start0();  // JVM 实现,真正创建线程

关键流程: start() 调用 native start0() → JVM 创建操作系统线程 → 新线程执行 run()。直接调 run() 绕过了这整条链路,只是普通方法调用。

三、调两次 start():直接报错

java 复制代码
Thread t = new Thread(task);
t.start(); // 正常启动
t.start(); // IllegalThreadStateException! 👈

看看源码为什么报错:

java 复制代码
public synchronized void start() {
    if (threadStatus != 0)  // 已经启动过,threadStatus != 0 👈
        throw new IllegalThreadStateException();
    ...
}

为什么不让调两次? 因为线程的生命周期是单向的

复制代码
NEW → RUNNABLE → (BLOCKED/WAITING/TIMED_WAITING) → TERMINATED

线程一旦启动,就回不到 NEW 状态。你没法让一个已经跑完的线程"复活",就像没法让烧完的蜡烛再点一次------你得拿一根新的。

生活类比: 火箭发射。倒计时结束、火箭升空,你不能对着同一个火箭再按一次发射按钮------要么它还在飞(已经启动了),要么已经烧完了(TERMINATED)。

四、正确姿势:需要新线程就创建新对象

java 复制代码
// ❌ 复用同一个 Thread 对象
Thread t = new Thread(task);
t.start();
t.start();  // 报错!

// ✅ 创建新的 Thread 对象
new Thread(task).start();
new Thread(task).start();  // 两个新线程,没问题 👈

// ✅ 更好的方式:用线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(task);
pool.submit(task);  // 线程池自动复用线程 👈

五、Thread 的状态机:为什么只能 start 一次

复制代码
                  start()
    NEW ──────────────────→ RUNNABLE
                               │
                    ┌──────────┼──────────┐
                    ↓          ↓          ↓
                BLOCKED    WAITING   TIMED_WAITING
                    │          │          │
                    └──────────┴──────────┘
                               │
                               ↓
                          TERMINATED

threadStatus 值对应:

  • 0 = NEW(还没 start)
  • 其他值 = 已启动(RUNNABLE/BLOCKED/WAITING/TERMINATED 等)

start() 检查的就是这个值,非 0 直接拒绝。

run() vs start() 全景

复制代码
run() vs start() 全景

start()
├── 作用:启动新线程
├── 底层:native start0() → 创建 OS 线程 → JVM 回调 run()
├── 只能调用一次,重复调用抛 IllegalThreadStateException
└── 线程状态:NEW → RUNNABLE

run()
├── 作用:在当前线程执行任务
├── 底层:普通方法调用,无新线程
├── 可以反复调用(就是普通方法)
└── 线程状态:无变化

线程生命周期(单向不可逆)
NEW → RUNNABLE → TERMINATED
           ↕
    BLOCKED / WAITING / TIMED_WAITING

口诀:start 建线程调一次,run 是内容可反复,
      线程生命周期单行道,start 过了不回头。

回答技巧与点评

标准回答

直接调用 run() 不会创建新线程,只是在当前线程中执行 run() 方法体,等同于普通方法调用。start() 才会调用 native start0() 创建操作系统线程,由 JVM 在新线程中回调 run()。调用两次 start() 会抛 IllegalThreadStateException,因为线程生命周期是单向的,start() 内部会检查 threadStatus,非 0(即已启动过)就抛异常。需要再次执行任务应该创建新的 Thread 对象或使用线程池。

加分回答
  1. 设计哲学:线程状态的单向流转是操作系统的底层设计------OS 线程一旦终止,其资源(栈、寄存器等)已被回收,Java 不可能在 JVM 层面"复活"一个 OS 线程。IllegalThreadStateException 是 Java 对底层不可逆性的上层表达
  2. Runnable 的优势:正是因为 Thread 不能重复 start,才更应该用 Runnable------任务是可复用的,Thread 对象是一次性的。把"任务"和"执行载体"分离,既省资源又灵活
  3. 线程池的设计动机:线程池的核心价值就是"线程复用"------避免了反复创建销毁 OS 线程的开销。Worker 线程跑完一个任务不退出,而是循环取下一个任务,本质上是绕过了"线程只能 start 一次"的限制
面试官点评

这道题考的是你对线程生命周期和底层机制的理解。能说出"run() 不创建新线程"只是入门,能讲清 start0() 的 native 调用、threadStatus 的状态检查、以及线程生命周期单向不可逆的设计原因,才说明你真正理解了线程的本质。如果你还能引出线程池的复用设计,面试官会认为你有全局视野。

原文阅读


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
逻辑驱动的ken9 小时前
Java高频面试考点场景题22
java·开发语言·jvm·面试·职场和发展·求职招聘·春招
落魄江湖行1 天前
孤舟笔记 并发篇二十九 volatile关键字有什么用?它的实现原理是什么?面试必问的轻量级同步机制
java并发·春招·孤舟笔记·volatile关键字
落魄江湖行1 天前
孤舟笔记 并发篇二十八 wait和sleep是否会触发锁的释放及CPU资源的释放?这个区别面试必考
java并发·春招·孤舟笔记·wait和sleep
落魄江湖行2 天前
孤舟笔记 并发篇二十二 线程池是如何回收线程的?核心线程和非核心线程的回收逻辑大不相同
java并发·春招·孤舟笔记·线程池是如何回收线程的
落魄江湖行2 天前
孤舟笔记 并发篇二十五 当任务数超过核心线程数时,如何让任务不进入队列?线程池调优的经典问题
java并发·春招·孤舟笔记·当任务数超过核心线程数时
落魄江湖行2 天前
孤舟笔记 并发篇二十三 线程池是如何实现线程复用的?Worker循环取任务的秘密远比你想象的精巧
java并发·春招·孤舟笔记
落魄江湖行4 天前
孤舟笔记 并发篇十一 行锁、间隙锁、临键锁傻傻分不清?MySQL InnoDB的锁其实就这三板斧
mysql·java并发·春招·孤舟笔记
落魄江湖行4 天前
孤舟笔记 并发篇十 ReentrantLock的公平锁和非公平锁是怎么实现的?这个设计藏着大智慧
java并发·春招·孤舟笔记
逻辑驱动的ken5 天前
Java高频面试考点场景题17
开发语言·jvm·面试·求职招聘·春招