1. 线程创建与启动方式对比
三种线程创建方式的核心差异
Java 中创建线程的三种主流方式,本质区别在于任务与线程的耦合程度以及能否拿到执行结果。继承 Thread 把任务写进线程对象本身;实现 Runnable 把任务从线程中剥离出来;实现 Callable 在剥离任务的基础上,还允许任务返回结果并抛出受检异常。
三种方式的完整代码实现
下面用一个统一示例演示三种方式:每个任务打印线程名和 1~3 的计数,Callable 额外返回一个字符串结果。所有代码可直接放入一个 Main.java 中运行。
import java.util.concurrent.*;
public class Main {
// ===== 方式一:继承 Thread =====
static class ThreadWay extends Thread {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + " [Thread] 计数=" + i);
}
}
}
// ===== 方式二:实现 Runnable =====
static class RunnableWay implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName()
+ " [Runnable] 计数=" + i);
}
}
}
// ===== 方式三:实现 Callable =====
static class CallableWay implements Callable<String> {
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= 3; i++) {
sum += i;
System.out.println(Thread.currentThread().getName()
+ " [Callable] 累加=" + sum);
}
return "Callable 返回结果: sum=" + sum;
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== 方式一:继承 Thread ===");
ThreadWay t1 = new ThreadWay();
t1.setName("线程A");
t1.start();
System.out.println("=== 方式二:实现 Runnable ===");
Thread t2 = new Thread(new RunnableWay(), "线程B");
t2.start();
System.out.println("=== 方式三:实现 Callable ===");
FutureTask<String> ft = new FutureTask<>(new CallableWay());
Thread t3 = new Thread(ft, "线程C");
t3.start();
// 获取 Callable 的返回值(会阻塞直到任务完成)
String result = ft.get();
System.out.println("主线程拿到返回值 -> " + result);
// 等待前两个线程结束
t1.join();
t2.join();
System.out.println("所有线程执行完毕");
}
}
执行顺序与返回值观察
运行上述代码时,你会观察到以下现象:
- 执行顺序不确定:线程 A、B、C 的打印顺序由操作系统调度决定,每次运行可能不同。三个线程的计数输出会交错出现,这是并发执行的正常表现。
- 返回值差异:线程 A 和 B 没有返回值,主线程无法获取它们的执行结果;线程 C 通过
FutureTask.get()返回了"Callable 返回结果: sum=6"。 - 阻塞行为:
ft.get()会阻塞主线程,直到线程 C 的call()执行完毕并返回结果。如果线程 C 尚未完成,主线程会一直等待。 - 异常处理:
Callable.call()可以抛出受检异常,异常会被封装在ExecutionException中,由ft.get()抛出;而Runnable.run()不能抛出受检异常,只能内部捕获处理。
验证建议:将
ft.get() 注释掉,观察主线程是否还会等待线程 C 完成;再在 CallableWay.call() 中故意抛出 new Exception("测试异常"),观察 ft.get() 抛出 ExecutionException 的行为。
三种方式的选择依据
| 对比维度 | 继承 Thread | 实现 Runnable | 实现 Callable |
|---|---|---|---|
| 任务与线程关系 | 耦合(任务即线程) | 解耦(任务独立) | 解耦(任务独立) |
| 能否继承其他类 | 不能(Java 单继承) | 可以 | 可以 |
| 是否有返回值 | 无 | 无 | 有(Future 获取) |
| 能否抛受检异常 | 不能 | 不能 | 能 |
| 配合线程池 | 不推荐 | 推荐(execute/submit) | 推荐(submit 返回 Future) |
| 适用场景 | 简单演示、单继承场景 | 通用任务、线程池首选 | 需要结果、需要异常传播 |
实践结论:实际开发中优先使用
Runnable 或 Callable 配合线程池,避免继承 Thread。需要返回值或异常传播时选 Callable,其余情况选 Runnable。三种方式都通过 start() 启动线程,直接调用 run() 只是普通方法调用,不会创建新线程。