孤舟笔记 并发篇十八 为什么启动线程不能直接调用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 的状态检查、以及线程生命周期单向不可逆的设计原因,才说明你真正理解了线程的本质。如果你还能引出线程池的复用设计,面试官会认为你有全局视野。

原文阅读


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

相关推荐
j7~11 天前
IT 面试指南:从准备到 Offer 的完整路线图(校招 / 实习通用
面试·职场和发展·秋招·春招·实习面试·it后端
~木雨1 个月前
Java 线程从生到死:创建三式、六态流转、中断协作与死锁活锁饥饿,一篇讲透
java·java并发·虚拟线程·死锁·线程状态·中断机制
weixin_440730502 个月前
线程02-并发串行-互斥锁-Semaphore-Event
python·thread
csdn_aspnet2 个月前
C# 多个串口多个线程发送数据和接收数据
c#·串口·thread·com·serialport
爱敲键盘的猴子3 个月前
Java并发编程 -- volatile
java·java并发
胖大和尚4 个月前
C++ 多线程编程的实现方式
c++·thread
rebibabo4 个月前
Java高并发底层原理(二十四)—— 线程中断如何让任务安全停止
线程安全·线程中断·java并发·协作式停止·线程池关闭·中断标记
小bo波4 个月前
使用Thread子类创建线程 VS 使用Runnable接口创建线程的区别
java·多线程·thread·并发编程·runnable
梦想不只是梦与想5 个月前
Python 中的线程(Thread)
python·线程·thread
落魄江湖行5 个月前
孤舟笔记 互联网常用框架篇一 Dubbo和Spring Cloud到底该选谁?优缺点对比一次讲透
春招·孤舟笔记·dubbo和spring cl