在Java并发编程的起点,几乎所有开发者都曾面临一个看似简单却至关重要的选择:是调用thread.start()还是直接调用thread.run()?这两行代码在行为上有着天壤之别,深刻理解Java Thread.start 和 run 方法区别,是避免编写“伪并发”代码、真正掌控多线程编程的基石。其核心价值在于厘清一个根本概念:start()是向JVM和操作系统发起创建新线程的请求,开启真正的并行执行流;而run()仅仅是对象的一个普通方法调用,仍在当前线程上下文中顺序执行。混淆二者,轻则导致程序逻辑错误,重则引发对并发模型理解的系统性偏差。本文将深入源码、线程状态与执行模型,为你彻底解析这一关键区别。
一、 现象直击:一个代码示例揭示的两种截然不同的世界

让我们从一个最简单的例子开始,直观感受两者的差异:
public class StartVsRunDemo { public static void main(String[] args) { Thread myThread = new Thread(() -> { System.out.println(“当前线程名称:” + Thread.currentThread().getName()); System.out.println(“这段代码正在运行中...”); });System.out.println(“\n=== 调用 run() 方法 ===”); myThread.run(); // 注意:这里是直接调用run() System.out.println(“\n=== 调用 start() 方法 ===”); myThread.start(); // 这里调用start() }
}
输出结果可能是:
=== 调用 run() 方法 === 当前线程名称:main 这段代码正在运行中...
=== 调用 start() 方法 === 当前线程名称:Thread-0 这段代码正在运行中...
这个简单的实验清晰地揭示了Java Thread.start 和 run 方法区别的第一个关键点:调用run()时,代码仍在主线程(main)中执行;而调用start()后,代码在一个名为“Thread-0”的新线程中执行。前者是同步的,后者是异步的。这是所有区别的根源。
二、 机制剖析:start()方法背后的JVM与操作系统之旅
start()方法是线程生命周期的唯一正确起点。它的工作流程是一个精密的协作过程:
1. 状态检查与设置:首先,start()方法会检查线程状态。一个线程只能被启动一次。如果线程不是NEW状态(即已启动过),它会立即抛出IllegalThreadStateException。这是为什么第二次调用start()会报错的原因。
2. 加入线程组:将当前线程添加到创建时指定的线程组中。
3. 调用native方法start0():这是最核心的一步。start0()是一个本地(Native)方法,它通过JNI(Java Native Interface)调用底层操作系统(如Linux的pthread库、Windows的线程API)的接口,请求操作系统分配一个新的系统线程(或轻量级进程)资源。
4. 线程调度就绪:新创建的系统线程被初始化,其入口点被设置为JVM内部的一个函数,该函数最终会调用Java线程对象的run()方法。此时,线程状态由NEW变为RUNNABLE(或就绪状态),等待操作系统调度器分配CPU时间片。
5. 异步执行:start()方法调用迅速返回,主线程继续执行后续代码。与此同时,新线程在某个不确定的时刻被CPU调度,开始并发地执行其run()方法中的逻辑。
这个过程清晰地展示了start()的“异步启动”本质。在“鳄鱼java”网站的《JVM线程模型深度解析》课程中,通过图表和本地源码追踪,生动再现了这一过程。
三、 本质辨析:run()方法只是一个普通的接口实现
与start()的复杂性相比,run()方法在机制上极其简单。查看Thread类的源码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
当你继承Thread并重写run(),或向Thread构造函数传递一个Runnable对象(即target)时,run()方法仅仅包含了你想要在新线程中执行的代码逻辑。
直接调用run(),就等同于调用任何一个类的实例方法:
- 它不会进行任何线程状态检查。
- 它不会与操作系统交互去创建新线程。
- 它不会改变当前线程的上下文。
- 它的执行是同步阻塞的:调用者必须等待
run()方法完全执行完毕,才能继续执行下一行代码。
因此,直接调用run()完全违背了使用线程的初衷——实现并发。它只是“看起来”执行了任务,实则仍在老路上顺序执行。
四、 线程状态视角:NEW、RUNNABLE与TERMINATED的转换
从Java线程状态机的角度,能更理论化地理解Java Thread.start 和 run 方法区别。
- NEW(新建):线程对象被创建,但尚未调用
start()。 - RUNNABLE(可运行):调用
start()后,线程进入此状态。它可能正在执行,也可能正在等待CPU时间片。 - TERMINATED(终止):
run()方法执行完毕或线程因异常退出。
关键区别在于:start()方法是触发线程从NEW状态转换到RUNNABLE状态的唯一合法途径。而直接调用run(),线程对象的状态自始至终都是NEW(如果你在调用run()前和调用后检查thread.getState(),会发现它没有变化),因为它从未作为一个独立的线程被启动过。
五、 常见误区与严重后果:为什么不能混淆?
混淆start()和run()会导致一系列问题:
1. “伪并发”与性能错觉:开发者误以为使用了多线程提升了性能,实际上所有代码仍在单线程中顺序执行,无法利用多核CPU的优势。在高并发场景下,这会导致程序吞吐量远低于预期。
2. 线程安全假象:在测试线程安全的代码时,直接调用run()无法模拟出真正的并发交错执行(interleaving),可能掩盖了深层的竞态条件(Race Condition)或数据不一致问题,给生产环境埋下隐患。
3. 资源与生命周期管理混乱:线程池(ExecutorService)等高级框架内部调用的是start()来管理线程。如果你提交的任务(一个Runnable)在其内部错误地直接调用了另一个线程对象的run(),会破坏框架的线程管理和回收机制。
六、 正确实践与扩展思考
黄金法则:如果你想启动一个新线程来并发执行任务,永远使用thread.start()。直接调用run()仅在你明确需要将该方法作为普通同步方法执行时才使用(这种场景极其罕见,通常意味着设计有问题)。
在现代Java中的实践:现在更推荐实现Runnable接口或Callable接口,并将任务提交给ExecutorService(线程池)来执行,而不是直接操作Thread对象。但底层原理不变,线程池的execute()或submit()方法最终还是会调用工作线程的start()来启动线程(或复用已启动的线程)。
// 现代推荐做法
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> { // 这里传递的是一个Runnable或Callable
System.out.println(“任务在线程: ” + Thread.currentThread().getName() + “ 中执行”);
});
// 线程池内部会负责调用 start() 或等效机制来运行你的任务
总结与思考
透彻理解Java Thread.start 和 run 方法区别,是区分“多线程代码”与“真正并发执行”的认知分水岭。start()是点燃火箭发射按钮,它引发了一系列复杂的系统级操作,最终让代码在新轨道上并行飞行;而run()仅仅是火箭的设计图纸,直接调用它就像在地面上翻阅图纸,火箭从未离开过发射台。请反思:在你的项目或学习过程中,是否曾因急于看到代码运行而直接调用了run(),从而错过了真正的并发世界?当你使用异步框架时,是否清楚其底层是如何触发新线程执行的?理解这个根本区别,是你构建健壮、高效并发应用的坚实第一步。
版权声明
本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。





