Skip to content
第 22 章 后端 ⏱ 12 分钟阅读

第 22 章:线程池 ​

学习目标 ​

  • 理解为什么"别每次现招工人",要用线程池
  • 用"团队 + 排队大厅"的模型看懂七大参数
  • 会套用企业级配置模板
  • 分清线程池管什么、不管什么

一、为什么必须用线程池? ​

上一章我们用 new Thread() 叫工人,但这样有个大问题:每次有活就现场招一个临时工,太贵了。

java
// ❌ 每来一个请求就 new 一个线程(现招临时工)
public void handle() {
    new Thread(() -> process()).start();   // 建线程要 1ms + 1MB 栈内存
}

现招工人的三个致命问题:

  • 贵:创建/销毁一个线程有开销(约 1ms + 1MB 栈),请求一多全花在招人上
  • 失控:没法限量,请求暴涨就无限招人 → 内存爆掉(OOM)
  • 没法管:不归你管,没法监控、没法优雅关闭

阿里规约:线程资源必须通过线程池提供,不允许显式 new Thread。

二、线程池 = 固定的团队 + 排队大厅 ​

线程池就是提前招好固定的几个工人,让他们排队干活:

任务来了 → 丢进【排队大厅】(队列) → 哪个工人空闲谁拿去干 → 干完继续等下一个
  • 工人不辞退,一直复用 → 又快又省
  • 能限量(最多招 N 个) → 不会爆内存
  • 归你管 → 能监控、能优雅关闭

对应关系:

术语团队比喻实际含义
核心线程数 corePoolSize正式编制的老员工平时常驻的工人
最大线程数 maximumPoolSize编制 + 允许临时外聘的人数上限最多能有几个工人
任务队列 workQueue排队大厅(等着干活的单子)装等待任务的队列
空闲存活时间 keepAliveTime临时工闲置多久被辞退非核心线程空闲多久回收
java
// 一个"5 人固定团队 + 排队大厅"的线程池
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.execute(() -> System.out.println("丢给团队,谁有空谁干"));   // 派活

三、这个团队的规矩:七大参数 ​

手动创建线程池最灵活,但参数多。用团队类比记:

java
new ThreadPoolExecutor(
    8,                                 // ① corePoolSize:正式工 8 个
    16,                                // ② maximumPoolSize:最多 16 个(8 正式 + 8 临时)
    60, TimeUnit.SECONDS,              // ③ ④ 临时工闲置 60 秒就辞退
    new ArrayBlockingQueue<>(1000),    // ⑤ 排队大厅能装 1000 个任务(有界!)
    namedThreadFactory(),              // ⑥ 给工人起名,方便排查(见下)
    new ThreadPoolExecutor.CallerRunsPolicy()  // ⑦ 大厅也满了怎么办(见第五节)
);

⑥ 给工人起名(排查线上问题全靠它):

java
private static ThreadFactory namedThreadFactory() {
    return new ThreadFactory() {
        private final AtomicInteger seq = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "biz-thread-" + seq.getAndIncrement());
        }
    };
}

四、任务来了怎么分配(提交流程) ​

当一个任务提交进来,团队按这个顺序处理:

任务来了
  ↓
正式工没干满?(当前线程 < corePoolSize)
  ├─ 是 → 开一个新工人去干
  └─ 否 → 排队大厅(队列)满了没?
         ├─ 没满 → 丢进大厅排队
         └─ 满了 → 还能招临时工?(当前线程 < maximumPoolSize)
                ├─ 是 → 招临时工去干
                └─ 否 → 大厅和人都满了 → 走【拒绝策略】

⚠️ 坑 1:大厅满了才会招临时工。如果你用了无界队列(LinkedBlockingQueue() 默认容量 Integer.MAX_VALUE),大厅永远装不满,临时工就永远招不出来,maximumPoolSize 形同虚设,任务堆积可能 OOM。

java
// ❌ 无界队列:大厅无限大,任务无限堆 → 内存爆
new ThreadPoolExecutor(8, 16, 60, SECONDS, new LinkedBlockingQueue<>());
// ✅ 有界队列:大厅限 1000,满了才招临时工
new ThreadPoolExecutor(8, 16, 60, SECONDS, new ArrayBlockingQueue<>(1000));

临时工招多少、闲置多久?

任务类型线程数公式为什么
CPU 密集(纯计算)CPU 核数 + 1多开也抢不到 CPU,反而切换浪费
IO 密集(网络/磁盘)2 × CPU 核数 或更多线程大多在等 IO,多开能填满空闲

拿核数:Runtime.getRuntime().availableProcessors()

五、团队满了怎么办:拒绝策略 ​

大厅满了 + 人也满了,新任务没地儿去,触发拒绝策略:

策略行为推荐度
AbortPolicy(默认)直接抛异常,任务丢掉⭐⭐⭐
CallerRunsPolicy让提交任务的线程自己去干(天然限流)⭐⭐⭐⭐⭐
DiscardPolicy静默丢弃(丢了都不知道)❌
DiscardOldestPolicy丢大厅里最老的任务,再试一次⭐⭐

生产推荐 CallerRunsPolicy:高峰期时,提交方(比如主线程)被迫自己干 → 它变慢 → 新请求进来得排队,自动"减速限流",不丢任务。

六、关键澄清:线程池管什么、不管什么 ​

新手最容易混淆的是这里。

线程池保证的是:

  • 一个线程同一时刻只干一个任务(干完才取下一个)
  • 所以同一个请求,绝不会同时被两个线程执行——一个请求分配一个线程,全程跑完

线程池不保证的是:

  • 不保证"两个不同的请求不碰同一份数据"。用户 A 和 B 同时改同一行,是两个请求 → 两个线程 → 同时执行同一句 SQL。线程池不管这个。

那怎么办?回上一章说的:改数据库有行锁兜底,你写普通 CRUD 根本不用管。线程池 + 数据库行锁,各管一段。

七、submit vs execute ​

java
pool.execute(() -> doWork());                   // ① Runnable,没返回值
Future<String> f = pool.submit(() -> "result"); // ② Callable,要返回值
String result = f.get();                        // 阻塞等结果

⚠️ 坑 2:pool.submit() 里抛的异常会被吞,只有 Future.get() 时才抛出来。

java
Future<?> f = pool.submit(() -> { throw new RuntimeException("炸了"); });
// 前面的代码看不到异常,只有 f.get() 才会抛

八、Executors 工厂方法(慎用) ​

JDK 自带简便方法,但多数有坑:

java
Executors.newFixedThreadPool(4);          // 固定团队 —— ⚠️ 无界队列
Executors.newCachedThreadPool();          // 自动伸缩 —— ⚠️ 无界队列
Executors.newSingleThreadExecutor();      // 单线程 —— ⚠️ 无界队列
Executors.newScheduledThreadPool(4);      // 定时任务

⚠️ 坑 3:newFixedThreadPool / newCachedThreadPool 内部都用无界队列,任务一多可能 OOM。生产建议自己 new ThreadPoolExecutor + 有界队列(见第三节模板)。

九、本章小结 ​

概念团队比喻关键
为什么用别每次现招人贵 / 失控 / 没法管
corePoolSize正式工人数常驻
maximumPoolSize最多工人数大厅满了才招临时工
workQueue排队大厅必须有界防 OOM
拒绝策略大厅人也满了生产推荐 CallerRunsPolicy
线程数—CPU 密集=核数+1,IO 密集=2×核数
管什么—一个请求一个线程
不管什么—数据冲突靠数据库行锁

动手练习 ​

  1. 建团队:用 ThreadPoolExecutor 建一个 5 人团队 + 有界队列,提交 10 个任务观察
  2. 大厅实验:把队列设成 2,一次提交 20 个任务,看拒绝策略怎么触发
  3. Future 取结果:submit 10 个 Callable,用 invokeAll 等全部完成
  4. 数工人:自定义 ThreadFactory 起名后,观察打印的线程名

下一章:第 23 章:并发容器与原子类 →

本站基于 VitePress 构建 · 由 StackHub 团队维护