1. 线程创建与启动的三种方式对比
三种创建方式的核心差异
Java 中创建并启动线程有三种常见方式,它们的本质区别在于任务如何定义以及线程如何被启动。无论哪种方式,最终都必须通过 Thread.start() 让 JVM 创建新线程并调用 run() 方法。
- 继承 Thread:把任务写在
run()重写中,创建子类实例后直接start()。缺点是 Java 单继承,任务无法再继承其他类。 - 实现 Runnable:把任务封装在
Runnable对象中,交给Thread执行。任务与线程解耦,推荐优先使用。 - 实现 Callable + FutureTask:任务可以有返回值、可以抛出受检异常,通过
FutureTask包装后仍由Thread启动,用get()获取结果。
完整可运行示例
下面代码分别用三种方式创建线程,每个线程打印自己的名称和序号,Callable 额外返回一个计算结果。运行后观察输出顺序和返回值。
import java.util.concurrent.*;
public class ThreadCreateDemo {
// 方式一:继承 Thread
static class MyThread extends Thread {
private final int id;
MyThread(int id) { this.id = id; }
@Override
public void run() {
System.out.println("[继承Thread] " + getName() + " 执行, id=" + id);
}
}
// 方式二:实现 Runnable
static class MyRunnable implements Runnable {
private final int id;
MyRunnable(int id) { this.id = id; }
@Override
public void run() {
System.out.println("[实现Runnable] " + Thread.currentThread().getName() + " 执行, id=" + id);
}
}
// 方式三:实现 Callable
static class MyCallable implements Callable<Integer> {
private final int id;
MyCallable(int id) { this.id = id; }
@Override
public Integer call() throws Exception {
System.out.println("[实现Callable] " + Thread.currentThread().getName() + " 执行, id=" + id);
return id * 100; // 返回计算结果
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== 主线程开始 ===");
// 方式一
MyThread t1 = new MyThread(1);
t1.setName("Thread-A");
t1.start();
// 方式二
Thread t2 = new Thread(new MyRunnable(2), "Thread-B");
t2.start();
// 方式三
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable(3));
Thread t3 = new Thread(futureTask, "Thread-C");
t3.start();
// 获取 Callable 返回值(阻塞直到 call() 执行完毕)
Integer result = futureTask.get();
System.out.println("Callable 返回值: " + result);
System.out.println("=== 主线程结束 ===");
}
}
运行结果与判读
一次典型运行输出如下(线程调度顺序可能不同):
=== 主线程开始 ===
[继承Thread] Thread-A 执行, id=1
[实现Runnable] Thread-B 执行, id=2
[实现Callable] Thread-C 执行, id=3
Callable 返回值: 300
=== 主线程结束 ===
判读要点:
- 三个线程的输出顺序不固定,取决于操作系统线程调度,多运行几次会看到不同排列。
futureTask.get()会阻塞主线程,直到 Thread-C 的call()返回。因此 "Callable 返回值: 300" 一定出现在 Thread-C 执行之后。- 如果把
t1.start()改成t1.run(),输出会变成主线程串行执行,且线程名是main,证明run()只是普通方法调用。
三种方式选择建议
| 对比维度 | 继承 Thread | 实现 Runnable | Callable + FutureTask |
|---|---|---|---|
| 任务定义位置 | 重写 run() | 实现 run() | 实现 call() |
| 是否有返回值 | 无 | 无 | 有,通过 get() 获取 |
| 能否抛受检异常 | 否 | 否 | 是 |
| 继承限制 | 占用唯一继承位 | 无 | 无 |
| 任务与线程耦合度 | 高(任务即线程) | 低(任务可复用) | 低(任务可复用) |
| 推荐场景 | 简单演示 | 大多数无返回值任务 | 需要结果或异常的任务 |
常见错误:调用
run() 而不是 start()。前者在当前线程中同步执行,不会创建新线程;后者才会让 JVM 创建新线程并异步执行 run()。这是初学者最容易混淆的点。