创建线程有几种方式?各有什么利弊?

#Java#面试八股

创建线程有几种方式?各有什么利弊?

结论先行:教材上说四种(继承 Thread、实现 Runnable、实现 Callable + FutureTask、线程池),但从 JVM 角度看只有一种:new Thread(...).start() 触发 start0() 这个 native 方法去创建 OS 线程。其余写法都只是在决定 run() 里执行什么。

1. 四种写法对照

方式写法要点能否拿返回值能否抛受检异常适用场景
继承 Thread重写 run(),直接 start()否否教学演示,实际不推荐
实现 Runnable传给 Thread 构造器否否无返回值的简单任务
实现 Callable + FutureTaskFutureTask 既是 Runnable 又是 Future是是需要结果或异常
线程池submit / execute视方法而定视方法而定生产环境唯一正确选择

2. 它们之间的关系

Runnable(run(): void)
├── Thread            implements Runnable,内部持有 target 字段
│   └── start0()      → JVM 创建 OS 线程,新线程再调用 run()
└── FutureTask        implements Runnable + Future,包装一个 Callable
    └── run()         → 调 call(),把返回值或异常存进 outcome

Callable(call(): V throws Exception)
└── 只能由 FutureTask 或线程池的 submit 包装执行

Thread 内部有个 target 字段。如果既继承了 Thread 又传了 Runnable,而子类重写了 run(),那么 target 会被完全忽略——任务悄悄不执行,这是继承 Thread 最阴的坑。

3. 四种写法的最小示例

// 1. 继承 Thread
class MyThread extends Thread {
    @Override public void run() { System.out.println("t1"); }
}
new MyThread().start();

// 2. 实现 Runnable(组合优于继承,任务可被多个线程共享)
Runnable r = () -> System.out.println("t2");
new Thread(r, "worker-1").start();

// 3. Callable + FutureTask:能拿返回值、能抛受检异常
FutureTask<Integer> task = new FutureTask<>(() -> {
    Thread.sleep(100);
    return 42;
});
new Thread(task).start();
Integer result = task.get(1, TimeUnit.SECONDS);   // 阻塞直到完成

// 4. 线程池(生产推荐)
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> 42);

4. start() 与 run() 的区别

Thread t = new Thread(() -> System.out.println(Thread.currentThread().getName()));
t.run();    // 输出 main —— 只是普通方法调用,仍在当前线程串行执行
t.start();  // 输出 Thread-0 —— 真正创建新线程

这是最高频的追问之一。start() 的语义是"启动一个新线程,由新线程去调用 run()",而直接调 run() 只是在当前线程里执行了一个方法。对一个已启动的 Thread 再调 start(),会抛 IllegalThreadStateException——JVM 只允许线程从 NEW 走一次。

5. 常见追问

追问一:为什么推荐实现 Runnable 而不是继承 Thread? Java 单继承,继承 Thread 就再也不能继承别的类;而且 Runnable 把"任务"和"执行机制"解耦,同一个任务实例可以交给线程池、也可以交给 FutureTask。这是组合优于继承的典型例子。

追问二:Callable 和 Runnable 的区别? Callable.call() 有返回值、可声明受检异常;Runnable.run() 都没有。线程池里 submit(Callable) 返回 Future,异常会被封装进 Future,调用 get() 时才以 ExecutionException 抛出。

追问三:JDK 21 虚拟线程算第几种方式? 算新的创建方式,但不是新的执行模型:Thread.ofVirtual().start(task) 或 Executors.newVirtualThreadPerTaskExecutor()。它仍然是 Thread 对象,只是不再一一对应 OS 线程。

追问四:Runnable 里的异常会怎样? 如果没人捕获,异常会沿线程栈冒到 Thread 的 UncaughtExceptionHandler,然后该线程终止,主线程毫不知情。这是"任务静默失败"的常见来源,务必给线程池配自定义 handler。

小结

记住三层:表象层有四种写法(Thread / Runnable / Callable / 线程池);机制层只有 start0() 一条路;工程层只有线程池一种正确答案,因为线程的创建、复用、拒绝、监控都得有人管。

笔记加载中…