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

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

三种创建方式的核心差异

Java 中创建并启动线程有三种常见方式,它们的本质区别在于任务如何定义以及线程如何被启动。无论哪种方式,最终都必须通过 Thread.start() 让 JVM 创建新线程并调用 run() 方法。

  • 继承 Thread:把任务写在 run() 重写中,创建子类实例后直接 start()。缺点是 Java 单继承,任务无法再继承其他类。
  • 实现 Runnable:把任务封装在 Runnable 对象中,交给 Thread 执行。任务与线程解耦,推荐优先使用。
  • 实现 Callable + FutureTask:任务可以有返回值、可以抛出受检异常,通过 FutureTask 包装后仍由 Thread 启动,用 get() 获取结果。
三种线程创建方式的结构对比 方式一:继承 Thread class MyThread extends Thread 重写 run() 定义任务 new MyThread().start() ✗ 单继承限制 ✗ 无返回值 方式二:实现 Runnable class MyTask implements Runnable 实现 run() 定义任务 new Thread(new MyTask()).start() ✓ 任务与线程解耦 ✗ 无返回值 方式三:Callable + FutureTask class MyCallable implements Callable<T> 实现 call() 返回结果 new Thread(new FutureTask<>(callable)).start() ✓ 有返回值 ✓ 可抛受检异常 共同本质:Thread.start() → JVM 创建新线程 → 调用 run() start() JVM 创建新线程 调用 run() 关键区别:直接调用 run() 只是普通方法调用,不会创建新线程;必须调用 start() 执行顺序由线程调度器决定,不可预测;Callable 的返回值通过 FutureTask.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实现 RunnableCallable + FutureTask
任务定义位置重写 run()实现 run()实现 call()
是否有返回值无无有,通过 get() 获取
能否抛受检异常否否是
继承限制占用唯一继承位无无
任务与线程耦合度高(任务即线程)低(任务可复用)低(任务可复用)
推荐场景简单演示大多数无返回值任务需要结果或异常的任务
常见错误:调用 run() 而不是 start()。前者在当前线程中同步执行,不会创建新线程;后者才会让 JVM 创建新线程并异步执行 run()。这是初学者最容易混淆的点。