真实面经题目 · 原创解析
Thread.start() 和 run() 有什么区别?
Thread.start() 的本质是请求 JVM 创建并启动一个新的操作系统线程,使线程对象从 NEW 进入 RUNNABLE,就绪后由调度器安排执行,最终在新线程中回调 run()。run() 本身只是 Thread 类上的普通实例方法,直接调用不会创建新线程,不会改变线程生命周期,只会在当前调用线程里同步执行方法体。
真实面经题目 · 原创解析
Thread.start() 的本质是请求 JVM 创建并启动一个新的操作系统线程,使线程对象从 NEW 进入 RUNNABLE,就绪后由调度器安排执行,最终在新线程中回调 run()。run() 本身只是 Thread 类上的普通实例方法,直接调用不会创建新线程,不会改变线程生命周期,只会在当前调用线程里同步执行方法体。
start() 是启动线程,run() 是线程要执行的任务入口;调用 start() 才会产生并发,直接调用 run() 只是普通方法调用。start() 会经过 JVM 的线程启动逻辑和 native 层创建真实执行线程,线程状态从 NEW 进入 RUNNABLE,之后由调度器决定何时运行,并在新线程中调用该 Thread 对象的 run()。run() 可以被当前线程反复当作普通方法调用,但不会让 Thread 对象进入 RUNNABLE,也不会产生新的调用栈。一个 Thread 对象只能 start 一次,重复 start 会抛 IllegalThreadStateException;而 run() 直接调用是否能多次执行,只取决于普通方法逻辑。异常方面,直接 run() 的异常会沿当前调用栈抛回调用者;start() 后新线程中的未捕获异常不会直接抛给启动者,而是交给 UncaughtExceptionHandler 等机制处理。工程实践中通常不直接继承 Thread 承载业务逻辑,而是把任务建模为 Runnable 或 Callable,交给线程池统一管理生命周期、复用线程和处理异常。
start() 和 run() 的差别不是名字上的启动与执行,而是执行语义完全不同。start() 表示把一个 Thread 对象交给 JVM 启动流程,让它拥有独立线程上下文和独立调用栈;run() 只是这个对象上的一个普通方法。直接调用 run() 时,代码仍然在当前线程里顺序执行,既没有并发,也没有新的线程生命周期变化。
Thread 对象创建后处于 NEW 状态,只有调用 start() 才能从 NEW 进入 RUNNABLE。这里的 RUNNABLE 在 Java 线程状态里包含就绪和运行两种调度层面的状态,也就是说 start() 返回并不代表 run() 已经开始执行,只代表线程已经具备被调度的资格。直接调用 run() 不会让 Thread 对象从 NEW 进入 RUNNABLE,调用结束后这个 Thread 对象仍然没有真正启动过。
start() 的关键价值在于触发 JVM 内部的线程启动逻辑,通常会进入 native 方法创建或关联底层操作系统线程,并准备线程栈、线程本地数据、调度相关元数据等运行环境。底层线程真正运行后,JVM 再在该新线程上下文中调用对应 Thread 对象的 run()。因此 run() 是任务入口,不是线程创建入口;线程创建入口是 start()。
直接调用 run() 时,调用栈属于当前线程,例如主线程调用 t.run(),那么 run() 方法体就在主线程栈中执行,调用者必须等它执行完才能继续向下走。调用 start() 时,启动者线程和新线程会形成两个独立执行流,启动者通常在 start() 返回后继续执行,而新线程何时执行 run() 由调度器决定,所以日志顺序、执行时机和数据竞争表现都会不同。
同一个 Thread 实例只能调用一次 start()。线程一旦被启动,它的生命周期就不可回到 NEW,即使 run() 已经执行结束并进入 TERMINATED,也不能再次 start,同一个对象重复 start 会抛 IllegalThreadStateException。这体现了 Thread 对象和一次真实线程生命周期的绑定关系。相对地,run() 作为普通方法可以被直接调用多次,但那不代表复用了或重启了线程。
异常处理是面试中很能体现理解深度的点。直接调用 run() 时,run() 内抛出的运行时异常会沿当前线程的普通调用栈传播,调用者可以像处理普通方法一样捕获。通过 start() 启动后,run() 发生未捕获异常时,异常发生在新线程里,不会自动抛回调用 start() 的线程,而是由该线程的 UncaughtExceptionHandler、线程组或运行环境的默认处理逻辑接管。
Thread 更像执行载体,Runnable 更像任务描述。把业务逻辑写进 Runnable,可以让任务和线程生命周期解耦,也方便交给不同执行器运行。继承 Thread 并重写 run() 也能实现多线程,但会把任务和线程对象绑定在一起,复用性和组合性较差。更推荐的表达是:创建 Runnable 描述要做什么,用 Thread 或线程池决定在哪里、何时、以什么策略执行。
实际项目里很少靠手写 new Thread().start() 管理大量并发任务,因为线程创建和销毁成本高,异常、排队、限流、拒绝策略、上下文传播都需要统一治理。线程池会复用工作线程,业务代码通常提交 Runnable 或 Callable。此时开发者关注的是任务边界、返回值、异常收集和资源隔离,而不是直接调用 run() 或频繁 start() 新线程。
Runnable task = () -> System.out.println(Thread.currentThread().getName());
new Thread(task, "worker-thread").start();
new Thread(task, "not-a-worker").run(); 因为 Thread 对象代表一次线程生命周期,启动后就不再处于 NEW 状态。线程终止后也不能回到 NEW,重复启动会破坏生命周期模型,所以 JVM 直接抛 IllegalThreadStateException。
不一定。start() 只是让线程进入 RUNNABLE,具备被调度资格。实际什么时候执行 run() 取决于操作系统调度、CPU 资源、优先级、锁竞争等因素。
最大风险是误以为产生了并发,实际代码仍在当前线程同步执行,可能导致主流程阻塞、测试误判、性能评估错误,也无法验证真实多线程下的竞态和可见性问题。
通常更推荐 Runnable 或 Callable。它们把任务逻辑和线程载体分离,便于线程池执行、复用任务、统一异常处理,也避免因为 Java 单继承限制导致设计僵硬。
线程池内部会维护一组已经启动的工作线程,工作线程循环从队列取任务,并在自己的线程上下文中调用任务的 run() 或 call()。业务方提交任务,不应该直接手动调用任务的 run() 来模拟异步执行。