线程池FAQ

线程池FAQ


0. 你将学到什么?

  • 线程池到底是什么、解决什么问题

  • 线程池怎么接任务→排队→扩容→执行→回收

  • SingleThreadExecutor 的作用与用法

  • 有界队列是啥,和 maximumPoolSize 有啥关系

  • 拒绝策略为什么存在、怎么选

  • 拒绝了怎么办(实操模板)

  • CPU 密集 vs I/O 密集,线程数到底设多少

  • 常见坑与“一键可用”代码模板


1. 线程池是什么?为啥不用“new Thread 走天下”?

一句话:线程池 = “可复用的工人队伍 + 等活的队列 + 排队/扩容/拒绝的规矩”。
解决三件事:

  1. 避免频繁创建/销毁线程的成本;

  2. 统一管理并发度,别把机器或下游打爆

  3. 有规矩(排队、背压、拒绝),系统高峰能优雅退让


2. 线程池怎么工作?(最关键的一张图)

            ┌──── submit(task) ────┐
            ▼                      │
运行线程 < corePoolSize ?  是 → 直接新建“核心线程”执行(不入队)
            │                      │ 否
            ▼                      │
        尝试入队 workQueue  —— 成功 → 排队等待
            │                      │ 失败(队列满了)
            ▼                      │
  运行线程 < maximumPoolSize ? 是 → 新建“非核心线程”执行这个新任务
            │                      │ 否
            └────────────→ 触发“拒绝策略”

细节点名:当队列满了时,新建的“非核心线程”先执行“本次提交的任务”,不是先去搬队列里的旧任务;等它跑完再到队列里拿下一份。这意味着全局顺序不保证(队列内部仍保持 FIFO)。


3. 什么是 SingleThreadExecutor?有什么用?

  • 定义:始终只有1 个工作线程,任务按提交顺序一个接一个跑;线程挂了会自动补一个

  • 用途

    • 顺序写日志/文件;

    • 单连接 I/O(串口/套接字);

    • 需要“同一时刻只允许一个任务执行”的场景;

    • 按 Key 串行,整体并行”可用多组 SingleThreadExecutor 做“分片串行”。

  • 创建(工程化版)

ThreadPoolExecutor one = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(1000),              // 有界:别让队列无限长
    r -> new Thread(r, "single-worker"),
    new ThreadPoolExecutor.CallerRunsPolicy()    // 背压:队满时调用方自己跑
);

4. 有界队列是啥?为什么“强烈建议用有界队列”?

有界队列 = 有上限的队列(如 ArrayBlockingQueue(1000))。
意义

  • 形成背压(满了就慢/拒),保护内存和尾延;

  • 配合拒绝策略,高峰期可控退让,不至于无声堆积。
    API 行为小抄

  • put阻塞直到有空位;

  • offer 立即失败(返回 false);

  • offer(timeout) 限时等待


5. maximumPoolSize 和队列到底啥关系?

  • 接纳顺序是核心→队列→非核心(到 max)→拒绝

  • 有界队列:队列不满就不扩容队列满了才创建非核心线程,直到 maximumPoolSize

  • 无界队列:几乎永远不满maximumPoolSize 基本形同虚设

  • SynchronousQueue(0 容量):不排队,来一个就尽快扩线程,最容易跑到 max(要防线程风暴)。

直观理解:队列像闸门。闸门开得大(队列大),扩容;闸门小,扩容。


6. 线程会并发到多少?和 CPU 核心是什么关系?

  • 最多同时并行执行的线程数 ≤ maximumPoolSize(和队列策略有关)。

  • CPU 逻辑核有关,但不等于:

    • CPU 密集max ≈ CPU核数(±1~2),再多只会增加上下文切换;

    • I/O 密集max大于 CPU 核数(甚至数倍),因为很多线程在等待 I/O不占 CPU,但要配超时/限流,别把下游打爆。

  • 粗估公式:需要线程数 ≈ Ncpu × (1 + 等待时间/计算时间)


7. 为什么会“拒绝任务”?拒绝策略有哪些?

为什么:当核心满 + 队列满 + 已达 maximumPoolSize,系统已过载。再接单只会:内存涨、尾延飙、下游崩。
四种内置策略

  • AbortPolicy(默认):抛异常,最“吵”,最好监控;

  • CallerRunsPolicy:把任务退给调用线程自己跑,形成自然背压

  • DiscardPolicy静默丢弃(风险大,谨慎用);

  • DiscardOldestPolicy:丢队首最旧的再尝试入队(会破坏全局顺序)。

选策略的关键:你的业务能不能丢?能不能等?要不要把“忙”反馈给调用方?(详见第 9 节)


8. 被拒绝了怎么办?(三类场景,给你模板就能用)

8.1 强一致/不可丢(如下单、转账)

  • 有限次重试 + 指数回退 + 幂等;仍失败→显式报忙(429/503)
static final int MAX_RETRY = 2;
T submitCritical(Callable<T> task, ThreadPoolExecutor pool) {
  int a=0; long back=20;
  for(;;){
    try { return pool.submit(task).get(800, TimeUnit.MILLISECONDS); }
    catch(RejectedExecutionException e){
      if(a++>=MAX_RETRY) throw new ServiceUnavailableException("busy");
      Thread.sleep(back + ThreadLocalRandom.current().nextLong(back/2+1));
      back = Math.min(back*2, 500);
    }
  }
}

8.2 同步接口,可降速(页面/网关)

  • CallerRunsPolicy:队满时让调用线程自己跑,RT 变长=用户可感知的背压

8.3 弱一致/可丢(埋点/异步日志)

  • 本地有界缓冲 + 批量 + 打点告警;缓冲满→丢弃或落盘。

9. 小白怎么配参数?(从 0 到 1 的“稳妥口径”)

  1. 先判断类型
  • CPU 密集:core ≈ Ncpumax ≈ Ncpu~Ncpu+2,队列小一点。

  • I/O 密集:core ≈ Ncpumax ≈ Ncpu×(2~8),队列中等有界,配超时/限流

  1. 给队列一个上限cap ≈ 目标QPS × 允许排队秒数(保留 30% 余量)。

  2. 拒绝策略:同步强一致→CallerRuns/Abort;弱一致→自定义降级或丢弃(一定打点)。

  3. 监控 5 指标:CPU/上下文切换、队列长度、拒绝次数、等待/执行 P95/P99、下游服务负载。

  4. 压测—收敛

  • P99 高→增 max 或缩短任务时间/限流;

  • CPU/切换高→降 max

  • 下游报警多→先限流,不是盲目加线程。

生产模板(通用 I/O 偏多)

int ncpu = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    ncpu, ncpu * 4,
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(2000),         // 有界
    r -> { Thread t = new Thread(r, "biz-pool"); return t; },
    new ThreadPoolExecutor.CallerRunsPolicy() // 背压
);
// 可选:平滑峰谷
// pool.allowCoreThreadTimeOut(true);

10. 常见坑(踩一次就会记住)

  • 无界队列 + 小 max:看似稳,实则高峰排队无限长,尾延/内存失控。

  • 在池线程里阻塞等同池任务Future#get()/join()):线程饥饿/死锁

  • 把 I/O 阻塞任务和 CPU 计算搅在一个池:互相拖垮,建议分池

  • ThreadLocal 不清理:线程复用导致“脏数据粘连”。

  • 静默丢弃:用 Discard 却没打点=“无声事故”。


11. FAQ:两句到位的正确认知

  • Q:core=4,就只能同时 4 个线程执行吗?
    A:不对。当队列满了,会新建“非核心线程”,最多到 maximumPoolSize 个并发线程。

  • Q:当队列满后新建线程会先清队吗?
    A:不会。先执行“当前提交的任务”,然后再从队列取后续。

  • Q:20 个 I/O 任务在 10 核 CPU 上是不是 10 个在等?
    A:看情况。很多线程可能在 I/O 阻塞(不占 CPU),CPU 甚至跑不满;如果计算占比高,就会排队争时间片


12. 一页“上线前检查清单”

  • new ThreadPoolExecutor(...)不要生产用 Executors.* 快捷池

  • 有界队列,容量基于 QPS×可接受排队时长

  • core/max/queue 与负载类型匹配(CPU vs I/O)

  • 拒绝策略明确且打点告警

  • 关键路径超时重试上限幂等

  • 指标齐全:活跃数、队列长、拒绝数、等待/执行 P95/P99、下游压力

  • 压测包含“队满/拒绝”场景

posted on 2025-11-11 23:07  滚动的蛋  阅读(0)  评论(0)    收藏  举报

导航