创建线程的几种方式?8 年 Java 开发:从业务场景到代码实战(附避坑指南)
作为一名摸爬滚打 8 年的 Java 开发,从刚入行时 “new Thread () 一把梭”,到后来电商高并发场景下 “线程池参数调优到凌晨”,再到现在微服务异步链路用 “CompletableFuture 链式调用”,创建线程的方式选不对,不仅会踩坑(比如 OOM、线程泄露),还会直接影响系统性能。
今天不聊空洞的理论,只从实际业务场景出发,带你搞懂 Java 中创建线程的 6 种常见方式 —— 每种方式都附 “业务场景 + 核心代码 + 优缺点 + 避坑点”,看完就能直接用到项目里。
一、先想清楚:为什么需要 “创建线程”?
新手容易上来就问 “怎么创建线程”,但八年开发的经验是:先明确业务需求,再选合适的方式。比如这些场景都需要多线程:
- 场景 1:电商下单后,异步发送短信 / 推送通知(不能让用户等通知发送完才看到下单成功);
- 场景 2:批量处理 10 万条订单数据(单线程处理要 10 分钟,多线程分拆后 1 分钟搞定);
- 场景 3:调用第三方接口(比如支付回调),需要超时重试(单独开线程避免阻塞主线程);
- 场景 4:计算订单总额(需要异步计算商品折扣、优惠券金额,最后汇总结果)。
不同场景对应不同的线程创建方式 —— 比如 “简单异步通知” 用线程池就行,“需要返回结果的计算” 就得用 Callable,“复杂异步链路” 必须上 CompletableFuture。
二、6 种创建线程的方式:从简单到实战
1. 方式 1:继承 Thread 类(新手入门级,生产用得少)
业务场景
最简单的单任务异步操作,比如 “打印用户下单日志”(不需要返回结果,逻辑简单)。
核心代码
// 1. 自定义线程类,继承Thread
class OrderLogThread extends Thread {
// 传入业务参数(比如订单ID)
private Long orderId;
public OrderLogThread(Long orderId) {
this.orderId = orderId;
}
// 2. 重写run()方法:线程要执行的业务逻辑
@Override
public void run() {
// 模拟打印日志(实际业务可能是写日志到ELK、数据库)
System.out.println("线程" + Thread.currentThread().getName() + ":打印订单日志,订单ID=" + orderId);
}
}
// 3. 调用方式
public class ThreadDemo {
public static void main(String[] args) {
// 新建线程对象
Thread logThread = new OrderLogThread(1001L);
// 启动线程(必须调用start(),不是run()!)
logThread.start();
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 代码简单,新手易理解 | 不能多继承(Java 单继承限制,扩展性差) |
| 直接操作 Thread 对象 | 业务逻辑与线程耦合(线程类既管业务又管线程) |
| 无法返回执行结果(run () 无返回值) |
八年开发解析
- 新手坑:直接调用
logThread.run()不会启动新线程!run()只是普通方法,必须用start()—— 它会告诉 JVM“启动一个新线程,执行 run () 里的逻辑”。 - 生产不用:这种方式耦合太高,比如后续要给线程加 “超时控制”“结果返回”,根本没法扩展,实际项目中几乎不用。
2. 方式 2:实现 Runnable 接口(解耦入门级,常用)
业务场景
多线程处理相同任务,比如 “批量处理 100 条订单,每个线程处理 10 条”(任务与线程分离,便于复用)。
核心代码
// 1. 实现Runnable接口:封装业务逻辑(任务)
class OrderProcessTask implements Runnable {
private List<Long> orderIds; // 要处理的订单ID列表
public OrderProcessTask(List<Long> orderIds) {
this.orderIds = orderIds;
}
// 2. 重写run():定义任务逻辑
@Override
public void run() {
String threadName = Thread.currentThread().getName();
for (Long orderId : orderIds) {
// 模拟处理订单(比如更新订单状态为“已处理”)
System.out.println("线程" + threadName + ":处理订单,ID=" + orderId);
}
}
}
// 3. 调用方式:用Thread包装Runnable任务
public class RunnableDemo {
public static void main(String[] args) {
// 准备100条订单ID
List<Long> allOrderIds = IntStream.rangeClosed(1, 100)
.mapToObj(Long::valueOf)
.collect(Collectors.toList());
// 分10个线程处理,每个线程处理10条
for (int i = 0; i < 10; i++) {
// 分片订单ID(第i个线程处理第i*10到(i+1)*10条)
int start = i * 10;
int end = Math.min((i + 1) * 10, allOrderIds.size());
List<Long> subOrderIds = allOrderIds.subList(start, end);
// 用Thread包装Runnable任务,启动线程
Thread taskThread = new Thread(new OrderProcessTask(subOrderIds));
taskThread.start();
}
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 解耦(任务与线程分离) | 无法返回执行结果(run () 无返回值) |
| 支持多实现(可再实现其他接口) | 不能直接启动,需用 Thread 包装 |
| 任务可复用(多个 Thread 可共用一个 Runnable) | 异常只能在 run () 内部处理,无法向外抛出 |
八年开发解析
- 核心改进:Runnable 把 “任务逻辑” 和 “线程控制” 分开了 —— 比如
OrderProcessTask只关心 “怎么处理订单”,至于用哪个线程执行、怎么启动,交给 Thread 去管,符合单一职责原则。 - 生产常用:简单异步任务(无返回值)几乎都用这种方式,比如异步发送短信、日志异步写入。
3. 方式 3:实现 Callable 接口(有返回值,进阶)
业务场景
需要异步获取执行结果的场景,比如 “计算订单总额(要累加商品金额、折扣、优惠券)”,每个计算步骤异步执行,最后汇总结果。
核心代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 1. 实现Callable接口:泛型指定返回值类型(这里是Double,订单总额)
class OrderAmountCalculateTask implements Callable<Double> {
private Long orderId;
public OrderAmountCalculateTask(Long orderId) {
this.orderId = orderId;
}
// 2. 重写call():有返回值,可抛异常
@Override
public Double call() throws Exception {
String threadName = Thread.currentThread().getName();
System.out.println("线程" + threadName + ":开始计算订单" + orderId + "的总额");
// 模拟复杂计算(比如调用商品服务查单价、折扣服务算优惠)
Thread.sleep(1000); // 模拟耗时操作
double goodsAmount = 299.9; // 商品金额
double discount = 50.0; // 折扣金额
double total = goodsAmount - discount;
System.out.println("线程" + threadName + ":订单" + orderId + "总额计算完成,结果=" + total);
return total; // 返回计算结果
}
}
// 3. 调用方式:用FutureTask包装Callable,获取结果
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// a. 创建Callable任务
Callable<Double> calculateTask = new OrderAmountCalculateTask(1001L);
// b. 用FutureTask包装(FutureTask实现了Runnable,可被Thread启动)
FutureTask<Double> futureTask = new FutureTask<>(calculateTask);
// c. 启动线程
new Thread(futureTask).start();
// d. 获取结果(get()会阻塞,直到任务完成;建议加超时,避免死等)
// 方式1:直接阻塞获取
Double orderTotal = futureTask.get();
// 方式2:超时获取(推荐!避免线程永久阻塞)
// Double orderTotal = futureTask.get(3, TimeUnit.SECONDS);
System.out.println("主线程:获取到订单总额=" + orderTotal);
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 可返回执行结果 | get () 方法会阻塞主线程(需注意超时) |
| 支持抛出异常(便于外层处理) | 代码较复杂(需 FutureTask 包装) |
| 无法直接取消任务(需结合 Future 的 cancel ()) |
八年开发解析
- 关键区别:Callable 和 Runnable 的核心差异是
call()有返回值且可抛异常 —— 这解决了 “异步任务需要结果反馈” 的痛点,比如支付回调后的结果校验、数据计算后的汇总。 - 避坑点:
futureTask.get()会阻塞主线程!如果任务执行时间长(比如 3 秒以上),主线程会一直等,影响性能。生产环境必须用get(long timeout, TimeUnit unit)加超时,超时后抛TimeoutException,避免死等。
4. 方式 4:线程池(ExecutorService)(生产环境首选)
业务场景
高并发场景,比如 “电商大促时,每秒 1000 个下单请求,每个请求后需异步发通知、扣积分”—— 手动创建线程会导致线程数暴增(OOM 风险),线程池可复用线程、控制并发数。
核心代码
import java.util.concurrent.*;
public class ThreadPoolDemo {
// 1. 手动创建线程池(推荐!避免用Executors,防止OOM)
private static final ExecutorService threadPool = new ThreadPoolExecutor(
5, // 核心线程数(常驻线程,即使空闲也不销毁)
10, // 最大线程数(核心线程满+队列满后,最多再创建的线程数)
60, // 空闲线程存活时间(非核心线程空闲超过这个时间会销毁)
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(100), // 任务队列(核心线程满后,任务先放队列)
Executors.defaultThreadFactory(), // 线程工厂(创建线程的方式,可自定义命名)
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略(队列满+最大线程满后,如何处理新任务)
);
// 2. 业务任务:异步发送订单通知
static class OrderNotifyTask implements Runnable {
private Long orderId;
public OrderNotifyTask(Long orderId) {
this.orderId = orderId;
}
@Override
public void run() {
String threadName = Thread.currentThread().getName();
try {
// 模拟调用短信接口发送通知
Thread.sleep(500);
System.out.println("线程" + threadName + ":给订单" + orderId + "发送通知成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断状态,避免线程池忽略中断
System.out.println("线程" + threadName + ":发送通知被中断,订单ID=" + orderId);
}
}
}
public static void main(String[] args) {
// 模拟大促场景:1000个下单请求,每个请求触发异步通知
for (int i = 1; i <= 1000; i++) {
Long orderId = Long.valueOf(i);
// 3. 提交任务到线程池(有两种方式:submit和execute)
// 方式1:execute提交Runnable(无返回值)
threadPool.execute(new OrderNotifyTask(orderId));
// 方式2:submit提交Callable(有返回值,返回Future)
// Future<Boolean> future = threadPool.submit(() -> {
// // 模拟发送通知并返回结果
// return true;
// });
}
// 4. 关闭线程池(生产环境一般不主动关,随应用生命周期)
// threadPool.shutdown(); // 平缓关闭:处理完已提交的任务,不再接受新任务
// threadPool.shutdownNow(); // 强制关闭:立即中断所有任务,可能导致数据不一致
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 线程复用(减少创建 / 销毁开销) | 参数配置复杂(核心线程数、队列、拒绝策略需结合业务调优) |
| 控制并发数(避免线程暴增 OOM) | 需手动关闭(否则应用退出时可能有任务残留) |
| 支持任务排队、拒绝策略(容错性强) | 线程池耗尽时,新任务会排队或被拒绝(需合理配置) |
八年开发解析
-
为什么不用 Executors?Executors 提供的
newFixedThreadPool()、newCachedThreadPool()有坑:newCachedThreadPool():最大线程数是Integer.MAX_VALUE,高并发下会创建无数线程,直接 OOM;newFixedThreadPool():队列是LinkedBlockingQueue(无界),任务堆积会导致队列满,同样 OOM。
生产环境必须手动创建 ThreadPoolExecutor,明确核心参数(尤其是队列和拒绝策略)。
-
核心参数怎么设?参考公式:
- CPU 密集型任务(比如计算):核心线程数 = CPU 核心数 + 1(充分利用 CPU,避免上下文切换);
- IO 密集型任务(比如发通知、查数据库):核心线程数 = CPU 核心数 * 2(IO 等待时线程可空闲,多开线程提高利用率)。
-
拒绝策略选哪个?
- 核心业务(下单):用
CallerRunsPolicy(让调用者线程执行任务,避免丢任务); - 非核心业务(日志):用
DiscardOldestPolicy(丢弃最老的任务,保留新任务)或DiscardPolicy(直接丢弃新任务)。
- 核心业务(下单):用
5. 方式 5:Lambda 表达式(简化代码,Java 8+)
业务场景
简化 Runnable/Callable 的代码,比如 “简单的异步任务,不需要单独定义类”(比如打印日志、简单计算)。
核心代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class LambdaThreadDemo {
public static void main(String[] args) {
// 1. 简化Runnable(结合Thread)
new Thread(() -> {
System.out.println("Lambda线程1:打印简单日志");
}).start();
// 2. 简化Runnable(结合线程池)
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(() -> {
System.out.println("Lambda线程2:处理简单任务");
});
// 3. 简化Callable(结合线程池)
pool.submit(() -> {
// 模拟计算并返回结果
return "Lambda线程3:计算结果=100";
});
pool.shutdown();
}
}
八年开发解析
- 本质:Lambda 是函数式接口的简化写法 ——Runnable(只有一个 run () 方法)、Callable(只有一个 call () 方法)都是函数式接口,所以可以用 Lambda 简写,省去单独定义类的麻烦。
- 生产建议:简单任务用 Lambda,复杂任务(比如 10 行以上代码)还是单独定义类 —— 避免 Lambda 代码过长,可读性差。
6. 方式 6:CompletableFuture(异步链式调用,Java 8+)
业务场景
复杂异步链路,比如 “下单流程:异步调用库存服务→异步调用支付服务→异步调用通知服务”,需要前一个任务的结果作为后一个任务的参数,且避免回调地狱。
核心代码
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CompletableFutureDemo {
// 自定义线程池(CompletableFuture默认用ForkJoinPool,高并发下建议自定义)
private static final ExecutorService customPool = Executors.newFixedThreadPool(5);
public static void main(String[] args) {
Long orderId = 1001L;
// 1. 异步调用库存服务(supplyAsync有返回值,对应Callable)
CompletableFuture<Boolean> stockFuture = CompletableFuture.supplyAsync(() -> {
System.out.println("线程" + Thread.currentThread().getName() + ":检查订单" + orderId + "库存");
// 模拟调用库存服务,返回是否有库存
return true; // 有库存
}, customPool)
// 2. 库存检查成功后,异步调用支付服务(thenApply接收前一个结果,返回新结果)
.thenApplyAsync(hasStock -> {
if (hasStock) {
System.out.println("线程" + Thread.currentThread().getName() + ":库存充足,发起支付");
// 模拟调用支付服务,返回支付是否成功
return true; // 支付成功
} else {
throw new RuntimeException("库存不足,支付失败");
}
}, customPool)
// 3. 支付成功后,异步调用通知服务(thenAccept接收前一个结果,无返回值)
.thenAcceptAsync(paySuccess -> {
if (paySuccess) {
System.out.println("线程" + Thread.currentThread().getName() + ":支付成功,发送订单通知");
// 模拟调用通知服务
}
}, customPool)
// 4. 异常处理(exceptionally捕获整个链路的异常,避免静默失败)
.exceptionally(ex -> {
System.err.println("异步链路异常:" + ex.getMessage());
return null;
});
// 等待链路完成(生产环境一般不用,让异步链路自己跑)
// stockFuture.join();
}
}
优缺点分析
| 优点 | 缺点 |
|---|---|
| 支持链式调用(避免回调地狱) | 学习成本高(API 多,比如 thenApply、thenAccept、whenComplete) |
| 可自定义线程池(性能可控) | 异常处理复杂(需显式用 exceptionally 或 whenComplete) |
| 支持并行任务(allOf、anyOf) | 调试难度大(异步链路栈信息不完整) |
八年开发解析
- 为什么用它?CompletableFuture 解决了 Future 的 “阻塞” 和 “回调地狱” 问题 —— 比如之前用 Future 做 3 个异步任务,需要依次 get () 阻塞;现在用
CompletableFuture.allOf(future1, future2, future3)就能并行执行,效率更高。 - 避坑点:CompletableFuture 默认用
ForkJoinPool.commonPool(),这个线程池是 JVM 共享的,高并发下会和其他任务(比如 Stream 并行流)抢资源,导致性能下降。生产环境必须自定义线程池,传入supplyAsync/thenApplyAsync的第二个参数。
三、八年开发的避坑指南:这些错别再犯!
- 不要直接调用 run () 方法:启动线程必须用
start(),run()只是普通方法,不会创建新线程; - 避免手动创建大量线程:比如循环
new Thread(),高并发下会导致线程数暴增,触发OutOfMemoryError: unable to create new native thread; - 线程池不要用 Executors:
newCachedThreadPool()、newFixedThreadPool()有 OOM 风险,手动创建ThreadPoolExecutor并指定队列和拒绝策略; - Future.get () 必须加超时:不加超时会导致主线程永久阻塞,生产环境用
get(3, TimeUnit.SECONDS); - CompletableFuture 要处理异常:默认异常会静默丢失(控制台不打印),必须用
exceptionally或whenComplete捕获; - 线程安全要注意:多线程共享变量(比如静态变量)必须加锁(
synchronized/Lock)或用线程安全类(ConcurrentHashMap、AtomicInteger),避免数据错乱。
四、总结:按业务场景选对方式
最后给一张 “选型对照表”,八年开发亲测好用:
| 业务场景 | 推荐方式 | 不推荐方式 |
|---|---|---|
| 简单无返回值任务(打印日志) | Runnable + 线程池 | 继承 Thread |
| 简单有返回值任务(计算结果) | Callable + 线程池 | FutureTask+Thread |
| 高并发任务(电商大促) | 线程池(ThreadPoolExecutor) | 手动 new Thread |
| 复杂异步链路(多服务调用) | CompletableFuture + 自定义线程池 | Future 链式 get () |
| 简化代码(Java 8+) | Lambda + 线程池 | 单独定义 Runnable 类 |
其实创建线程的核心不是 “会多少种方式”,而是 “能不能根据业务选对方式”—— 比如单线程能搞定的不用多线程,多线程能搞定的不用线程池,线程池能搞定的不用 CompletableFuture。适合业务的,才是最好的。
4万+

被折叠的 条评论
为什么被折叠?



