1. 线程创建与启动方式对比

线程创建与启动方式对比 学习卡片

三种线程创建方式的核心差异

Java 中创建线程的三种主流方式,本质区别在于任务与线程的耦合程度以及能否拿到执行结果。继承 Thread 把任务写进线程对象本身;实现 Runnable 把任务从线程中剥离出来;实现 Callable 在剥离任务的基础上,还允许任务返回结果并抛出受检异常。

继承 Thread 实现 Runnable 实现 Callable class MyThread extends Thread 重写 run() 方法 new MyThread() 创建线程对象 t.start() 启动线程 无返回值 无法抛出受检异常 适用场景 简单演示、单继承场景 不推荐用于实际项目 class MyTask implements Runnable 实现 run() 方法 new Thread(new MyTask()) 任务与线程解耦 t.start() 启动线程 无返回值 无法抛出受检异常 适用场景 推荐方式,任务可复用 配合线程池使用 class MyCall implements Callable<T> 实现 call() 方法 FutureTask<T> ft = new FutureTask<>(new MyCall()) new Thread(ft).start() 启动线程 有返回值 ft.get() 获取结果,可抛异常 适用场景 需要返回结果或抛异常 配合线程池 submit() 使用 核心结论:优先选 Runnable(解耦、可复用);需要返回值时选 Callable + FutureTask;继承 Thread 仅用于极简场景

三种方式的完整代码实现

下面用一个统一示例演示三种方式:每个任务打印线程名和 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() 只是普通方法调用,不会创建新线程。