创建线程有几种方式?各有什么利弊?
结论先行:教材上说四种(继承 Thread、实现 Runnable、实现 Callable + FutureTask、线程池),但从 JVM 角度看只有一种:new Thread(...).start() 触发 start0() 这个 native 方法去创建 OS 线程。其余写法都只是在决定 run() 里执行什么。
1. 四种写法对照
| 方式 | 写法要点 | 能否拿返回值 | 能否抛受检异常 | 适用场景 |
|---|---|---|---|---|
继承 Thread | 重写 run(),直接 start() | 否 | 否 | 教学演示,实际不推荐 |
实现 Runnable | 传给 Thread 构造器 | 否 | 否 | 无返回值的简单任务 |
实现 Callable + FutureTask | FutureTask 既是 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() 一条路;工程层只有线程池一种正确答案,因为线程的创建、复用、拒绝、监控都得有人管。