创建线程有几种方式?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 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/
面试考察点
-
基础语法掌握:面试官不仅仅是想知道你背了几种方式,更是想看你能不能把每种方式的特点、区别、适用场景说清楚。很多人只能列出名字,一追问就露馅。
-
底层原理理解:考察你是否理解这些创建方式本质上都是一样的——最终都通过
Thread类来创建线程。所谓 "不同方式",只是任务定义的形式不同罢了。 -
实战经验:能否结合实际场景说明选择哪种方式,以及为什么。如果你只会说 "四种方式" 然后罗列一遍,面试官大概率会觉得你只是背了八股文。
核心答案
先说结论:Java 创建线程主要有 4 种方式,如果算上 CompletableFuture 和 ForkJoinPool 这些可以扩展到 6 种——
| 方式 | 核心类/接口 | 特点 | 推荐场景 |
|---|---|---|---|
继承 Thread 类 | Thread | 简单直接,但 Java 单继承 | 简单演示、不推荐生产 |
实现 Runnable 接口 | Runnable | 避免单继承限制,最经典 | 通用场景 |
实现 Callable 接口 | Callable + FutureTask | 支持返回值和异常 | 需要获取执行结果 |
| 线程池 | ExecutorService | 复用线程、资源可控 | 生产环境首选 |
CompletableFuture | CompletableFuture | 异步编排、链式调用 | 异步任务组合(JDK 8+) |
ForkJoinPool | ForkJoinPool | 分治递归、工作窃取 | 并行计算场景 |
重点来了:不管你用哪种方式,底层都是创建了一个 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);
Callable 和 Runnable 的核心区别:
| 对比项 | Runnable | Callable |
|---|---|---|
| 返回值 | 无(void) | 有(泛型 V) |
| 异常 | 不能抛出受检异常 | 可以抛出受检异常 |
| 配套 | Thread 直接执行 | 需 FutureTask 或线程池包装 |
| 方法名 | run() | call() |
FutureTask 同时实现了 Runnable 和 Future 接口,所以它可以交给 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()并保存结果。 -
方式四:则是在外部包了一层 "池化管理",内部依然是通过
Thread和Runnable来工作,只不过加上了线程复用、任务队列、拒绝策略等生产级特性。
关键点在于:理解了这个本质之后,面试中就不会被 "到底有几种方式" 这个问题困住了。因为形式上可以有很多种(CompletableFuture、ForkJoinPool、Stream.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(),也可以传入自定义线程池。这块在异步编程场景下特别好用,面试的时候提一嘴绝对加分。
面试高频追问
start()和run()的区别?- 调用
start()会创建新线程并执行run();直接调用run()只是普通方法调用,不会创建新线程,在当前线程中同步执行。
- 调用
Runnable和Callable的区别?- 核心就两个:返回值和异常。
Callable有返回值、能抛受检异常,Runnable都不行。
- 核心就两个:返回值和异常。
- 为什么推荐用
Runnable而不是继承Thread?- 三个理由:避免单继承限制、任务与线程解耦、方便配合线程池使用。
常见面试变体
- "说说
start()和run()的区别?" - "
Runnable和Callable有什么不同?" - "你平时项目中怎么创建线程?为什么?"
记忆口诀
四种方式:继承 Thread、实现 Runnable、实现 Callable、线程池创建。本质都是 Thread + start()。
总结
创建线程的方式可以列很多,但面试回答抓住两点就够了:形式上知道 4 种基本方式加 CompletableFuture,本质上理解它们最终都通过 Thread 类创建线程。生产环境首选线程池,这个必须强调。