创建线程有几种方式?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

  1. 基础语法掌握:面试官不仅仅是想知道你背了几种方式,更是想看你能不能把每种方式的特点、区别、适用场景说清楚。很多人只能列出名字,一追问就露馅。

  2. 底层原理理解:考察你是否理解这些创建方式本质上都是一样的——最终都通过 Thread 类来创建线程。所谓 "不同方式",只是任务定义的形式不同罢了。

  3. 实战经验:能否结合实际场景说明选择哪种方式,以及为什么。如果你只会说 "四种方式" 然后罗列一遍,面试官大概率会觉得你只是背了八股文。

核心答案

先说结论:Java 创建线程主要有 4 种方式,如果算上 CompletableFutureForkJoinPool 这些可以扩展到 6 种——

方式核心类/接口特点推荐场景
继承 ThreadThread简单直接,但 Java 单继承简单演示、不推荐生产
实现 Runnable 接口Runnable避免单继承限制,最经典通用场景
实现 Callable 接口Callable + FutureTask支持返回值和异常需要获取执行结果
线程池ExecutorService复用线程、资源可控生产环境首选
CompletableFutureCompletableFuture异步编排、链式调用异步任务组合(JDK 8+)
ForkJoinPoolForkJoinPool分治递归、工作窃取并行计算场景

重点来了:不管你用哪种方式,底层都是创建了一个 Thread 对象。区别只在于 "任务的载体" 不同。

深度解析

一、继承 Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行:" + Thread.currentThread().getName());
    }
}

// 使用
new MyThread().start();

这是最直白的方式,直接继承 Thread 并重写 run() 方法。但问题是 Java 只支持单继承,你的类继承了 Thread 就不能再继承其他类了。实际开发中基本不用,面试的时候别主动推荐这个。

二、实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行:" + Thread.currentThread().getName());
    }
}

// 使用
new Thread(new MyRunnable()).start();

// Lambda 写法(JDK 8+)
new Thread(() -> System.out.println("线程运行:" + Thread.currentThread().getName())).start();

这个比继承 Thread 好在哪?关键就是 解耦——任务逻辑和线程机制分开了。Runnable 只负责定义 "做什么",Thread 负责 "怎么执行"。而且用 Lambda 表达式写起来非常简洁。

三、实现 Callable 接口

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "执行结果:" + LocalDateTime.now();
    }
}

// 使用 FutureTask 包装
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();

// 获取结果(会阻塞直到任务完成)
String result = futureTask.get();
System.out.println(result);

CallableRunnable 的核心区别:

对比项RunnableCallable
返回值无(void有(泛型 V
异常不能抛出受检异常可以抛出受检异常
配套Thread 直接执行FutureTask 或线程池包装
方法名run()call()

FutureTask 同时实现了 RunnableFuture 接口,所以它可以交给 Thread 执行,又能通过 .get() 获取结果。

四、线程池(生产环境首选)

// 手动创建线程池(推荐)
ExecutorService executor = new ThreadPoolExecutor(
    2,                      // 核心线程数
    5,                      // 最大线程数
    60L,                    // 空闲线程存活时间
    TimeUnit.SECONDS,       // 时间单位
    new ArrayBlockingQueue<>(100),  // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// 提交 Runnable 任务
executor.execute(() -> System.out.println("execute 方式"));

// 提交 Callable 任务
Future<String> future = executor.submit(() -> "submit 方式的返回值");
System.out.println(future.get());

线程池是生产环境的不二之选。为啥?因为 线程是昂贵的系统资源,频繁创建销毁的开销很大。线程池复用线程、控制并发数、提供任务队列和拒绝策略,这些都是生产级应用必须考虑的。

这块如果展开讲内容很多,面试官大概率会顺着追问线程池的参数和执行流程,这里先按下不表。

五、本质原理——所有方式殊途同归

上图展示了 4 种线程创建方式的本质。整体可以归纳为以下几点:

  • 殊途同归:不管哪种方式,最终都是通过 new Thread() 创建线程对象,调用 start() 方法启动线程。区别只在于任务(Runnable / Callable)的定义和传递方式不同。

  • 方式一:直接在 Thread 子类中写任务逻辑,任务和线程紧耦合。

  • 方式二:把任务抽象为 Runnable,通过构造函数传给 Thread,实现了任务和线程机制的解耦。

  • 方式三:在 Runnable 的基础上增加了返回值能力,通过 FutureTask 这个中间层做适配——FutureTask 实现了 Runnable 接口,内部持有一个 Callable,在 run() 方法中调用 Callable.call() 并保存结果。

  • 方式四:则是在外部包了一层 "池化管理",内部依然是通过 ThreadRunnable 来工作,只不过加上了线程复用、任务队列、拒绝策略等生产级特性。

关键点在于:理解了这个本质之后,面试中就不会被 "到底有几种方式" 这个问题困住了。因为形式上可以有很多种(CompletableFutureForkJoinPoolStream.parallel() 等),但底层的线程创建机制始终是统一的。

六、面试加分项——CompletableFuture(JDK 8+)

// 异步执行,无返回值
CompletableFuture<Void> cf1 = CompletableFuture.runAsync(() -> {
    System.out.println("异步任务执行");
});

// 异步执行,有返回值
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
    return "异步结果";
});

// 链式编排
cf2.thenApply(result -> result + " -> 加工")
   .thenAccept(System.out::println);

CompletableFuture 底层默认用的是 ForkJoinPool.commonPool(),也可以传入自定义线程池。这块在异步编程场景下特别好用,面试的时候提一嘴绝对加分。

面试高频追问

  1. start()run() 的区别?
    • 调用 start() 会创建新线程并执行 run();直接调用 run() 只是普通方法调用,不会创建新线程,在当前线程中同步执行。
  2. RunnableCallable 的区别?
    • 核心就两个:返回值和异常。Callable 有返回值、能抛受检异常,Runnable 都不行。
  3. 为什么推荐用 Runnable 而不是继承 Thread
    • 三个理由:避免单继承限制、任务与线程解耦、方便配合线程池使用。

常见面试变体

  • "说说 start()run() 的区别?"
  • "RunnableCallable 有什么不同?"
  • "你平时项目中怎么创建线程?为什么?"

记忆口诀

四种方式:继承 Thread、实现 Runnable、实现 Callable、线程池创建。本质都是 Thread + start()

总结

创建线程的方式可以列很多,但面试回答抓住两点就够了:形式上知道 4 种基本方式加 CompletableFuture本质上理解它们最终都通过 Thread 类创建线程。生产环境首选线程池,这个必须强调。