元数据卡
- 前置知识:并发同步(ch14-concurrency-sync)、线程创建(ch14-concurrency-intro)
- 预计时间:20 分钟
- 完成标志:能用 ExecutorService 管理并发任务,理解线程池核心参数,知道如何优雅关闭
铁匠铺生意好了。每来一个客人,你就招一个学徒。来 100 个客人你就招 100 个学徒——但工坊只有 4 个工位。更糟的是,招一个学徒要准备工具、交待规矩——开销比打铁本身还大。
每次都 new Thread() 来跑任务,就有这个问题:线程创建和销毁有成本,大量短线程甚至拖垮系统。
线程池:提前招好工人
方案很简单:提前招好 4 个工人,让他们站在工位前等着。来了任务就干,干完了等下一个。
java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
int taskId = i;
pool.submit(() -> {
System.out.println("任务 " + taskId
+ " 由 " + Thread.currentThread().getName() + " 执行");
try { Thread.sleep(500); } catch (InterruptedException e) { }
});
}
pool.shutdown();
System.out.println("主线程结束——池子里的任务还在跑");
}
}语言: Java 5+ 运行: 编译运行 预期输出:
任务 1 由 pool-1-thread-1 执行
任务 2 由 pool-1-thread-2 执行
...
主线程结束——池子里的任务还在跑
任务 5 由 pool-1-thread-1 执行 ← 线程复用!
...Executors 工厂方法
Executors 提供了几种预设线程池:
| 工厂方法 | 线程数 | 队列 | 什么时候用 |
|---|---|---|---|
newFixedThreadPool(n) | 固定 n 个 | 无界队列 | 任务量可控 |
newCachedThreadPool() | 动态增减 | 不排队 | 大量短任务 |
newSingleThreadExecutor() | 1 个 | 无界队列 | 顺序执行 |
newScheduledThreadPool(n) | 固定 n 个 | — | 定时/周期任务 |
Callable 和 Future:任务有返回值
有时你需要任务返回结果:
java
import java.util.concurrent.*;
public class CallableDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> future = pool.submit(() -> {
Thread.sleep(1000);
return 42;
});
System.out.println("主线程继续做别的...");
Integer result = future.get(); // 阻塞等结果
System.out.println("结果: " + result);
pool.shutdown();
}
}语言: Java 5+ 运行: 编译运行 预期输出:
主线程继续做别的...
结果: 42Future.get() 阻塞等待结果,就像 Thread.join() 但能拿返回值。任务抛异常时,get() 会抛出 ExecutionException。
ThreadPoolExecutor:精确控制
Executors 底层调的就是 ThreadPoolExecutor。直接用它,你能精确控制每个参数:
java
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列——避免 OOM
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);语言: Java 5+
线程池的工作流程:
任务 → 核心线程有空闲? → 直接干
↓ 没空闲
队列没满? → 排队
↓ 满了
总线程 < maximum? → 开新线程
↓ 满了
执行拒绝策略四种拒绝策略:AbortPolicy(抛异常,默认)、CallerRunsPolicy(谁提交谁干)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃最老任务)。
生产环境最佳实践
- 避免
Executors.newFixedThreadPool——它用无界队列,任务堆积可能 OOM。用ThreadPoolExecutor加有界队列。 - 线程数估算:CPU 密集型 = CPU 核数 + 1;IO 密集型 = CPU 核数 * 2(经验值,需压测)。
- 务必 shutdown()。
shutdown()优雅关闭,shutdownNow()发中断信号。 submit()vsexecute():前者返回Future包装异常;后者不返回结果,异常由未捕获异常处理器处理。
ForkJoinPool:分治任务
ForkJoinPool 专为可递归拆分的任务设计。日常用 parallelStream 即可:
java
int sum = IntStream.range(1, 1_000_001)
.parallel() // 底层 ForkJoinPool
.sum();
System.out.println(sum); // 500000500000语言: Java 8+
常见陷阱
- 线程池里抛异常不会被外层捕获——
submit()的异常在future.get()时才抛出。 - 忘了 shutdown() 会导致程序不退出。用
try { ... } finally { pool.shutdown(); }包住。
旅人笔记
线程池提前招好工人,任务来了直接干。Executors 快捷方便但有坑。生产环境用 ThreadPoolExecutor 精确控制核心参数。Future.get() 阻塞拿结果。务必 shutdown() 优雅关闭。
→ 下一步:I/O 与文件
程序不能只活在内存里。下一章看看怎么读写文件——I/O 操作也是并发场景最常见的搭档。