假设你是一家餐厅的老板。如果不设厨师团队,每位顾客点菜后你都临时去街上雇一位厨师,菜的原料还没备好、锅灶还没热,顾客已经等得不耐烦了——更可怕的是,高峰期一百个顾客就是一百次雇人,光办入职手续就能把后厨挤爆。但如果你固定养几位厨师,忙不过来时安排客人排队等位,实在挤不下再由领班出面劝退,整个餐厅就能在“服务速度”和“运营成本”之间找到平衡。 线程池就是这么一家饭店。在服务高并发请求时,操作系统创建和销毁线程的开销极大(雇人和辞退都很贵),所以聪明的做法是预先“养”一组线程,让任务像顾客一样排队入场。线程池的设计核心在于:养多少厨师(核心线程数)、最多容忍多少临时工(最大线程数)、等位区能站多少人(任务队列)、满客时怎么应对(拒绝策略)、如何给厨师发工牌(线程工厂)、怎样根据客流调班(动态调优)、以及如何从门店报表中发现问题(监控预警)。把这七个环节想透了,你就掌握了高并发系统“以小博大”的骨架。

岗位关联

学懂线程池设计,你就能在面试和实战中自信地回答“系统为什么卡顿”“如何扛住双十一流量”,这是后端与 AI 服务端工程师的核心基本功。

目录
  1. 可视化演示
  2. 线程池概要
  3. 核心参数设计
  4. 任务队列机制
  5. 拒绝策略取舍
  6. 线程工厂定制
  7. 动态调优秘诀
  8. 监控预警体系
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态展示线程复用机制、核心参数配置、任务提交流程与CPU/IO调优策略。

本节配有交互动效(进入视口后自动加载,约 3KB)。

线程池概要

假设你是一家小饭馆的老板,最初是顾客点一道菜你就跑街边雇一位师傅来炒。雇人要谈价、要等师傅换衣服、炒完还要结账送走——顾客早饿跑了,你的利润也全花在了“入职手续”上。后来你想通了:不如固定养三位师傅,不管有没有客人都在后厨待命,忙时加菜、闲时备料。线程池的逻辑与此完全一致:预先创建一组常驻线程,让任务轮流使用它们,而不是每次请求都新建一个线程。

在 Java 里最常用的是 ThreadPoolExecutor,在 Python 里对应 concurrent.futures.ThreadPoolExecutor。它们都遵循同一条工作路径:任务提交后,先找空闲核心线程;没有空闲核心线程就放进任务队列等待;队列也满了才考虑创建非核心线程;连非核心线程都用尽了,就触发拒绝策略。这条路径就像饭店的客流规则:先找空闲厨师,没厨师就安排等位,等位区满了才考虑叫临时工,临时工也不够就只能请客人改天再来。

这个“先复用、再排队、后扩容、终拒绝”的顺序,是线程池的灵魂。它保证了系统在任何流量冲击下都不会因为无限创建线程而崩溃。线程的创建与销毁是真正的开销大户——每次创建都要分配内核资源、建立线程栈;而线程池把这份成本一次性付清,后续只需重复使用,这就是所谓的“池化收益”。

回到饭店的比喻:与其让一百个顾客各雇一位厨师,不如养三位师傅、备一口大锅,出菜速度反而更快、成本更低。线程池的价值就藏在这笔“规模账”里。

Tpool<N×(Tcreate+Tdestroy)T_{\text{pool}} < N \times (T_{\text{create}} + T_{\text{destroy}})
这个公式想表达:对 N 个任务,按需创建线程的总成本远高于使用线程池复用的成本。
  1. 池化总成本线程池处理 N 个任务的实际开销,包含执行时间和极小的排队切换时间,通常远小于逐个创建线程。
    TpoolT_{\text{pool}}
  2. 任务数量系统需要处理的请求或任务总数,N 越大池化收益越明显。
    NN
  3. 创建与销毁成本每个线程从无到有、再从有到无消耗的内核资源与时间,这是高并发下最容易被低估的开销。
    Tcreate+TdestroyT_{\text{create}} + T_{\text{destroy}}

核心参数设计

饭店要定员,线程池要定参。ThreadPoolExecutor 有三大关键参数:核心线程数 corePoolSize、最大线程数 maximumPoolSize、空闲存活时间 keepAliveTime。你可以这样理解:核心线程是饭馆的正式厨师,不管有没有客人都有底薪、必须养着;最大线程数则规定了紧急时刻最多能招多少临时厨师;存活时间决定临时厨师在闲置多久后会被辞退。

这三者共同构成线程池的“容量公式”。当任务量小时,只有核心厨师在干活;任务逐渐增多,先排队而不是立刻雇人;当等位区满了,才开始招临时厨师(创建非核心线程);临时厨师闲下来的时间超过了 keepAliveTime,就会被辞退,把资源还回去。allowCoreThreadTimeOut(true) 还能让正式厨师在彻底没活时也被“放假”,适合流量极低但不想关闭线程池的场景。

很多初学者以为“最大线程数越大越好”,这就像饭店旺季疯狂招人一样危险——人多了互相碍事(上下文切换开销暴增),工资成本(内存占用)也受不了。参数设计的本质是给系统画一条“安全水位线”,让它在任何流量下都留在可控区间内。

把容量公式贴在饭店门口,你就知道高峰期最多能接多少客人:正式厨师 + 等位区 + 临时厨师。任何超出这个容量的流量,都必须被挡在门外——下一节我们就聊等位区。

Cpool=Tcore+Qmax⁡+(Tmax⁡−Tcore)C_{\text{pool}} = T_{\text{core}} + Q_{\max} + (T_{\max} - T_{\text{core}})
这个公式揭示线程池的“消化能力”由三部分构成:正式厨师、等位区容量、可扩张的临时厨师数量。
  1. 整体容量线程池在彻底饱和前能同时承载的任务总数,是判断系统水位的核心指标。
    CpoolC_{\text{pool}}
  2. 正式厨师数常驻线程数,平时只靠这批线程处理任务。
    TcoreT_{\text{core}}
  3. 等位区容量任务队列最多能暂存多少个任务,这是缓冲冲击的关键。
    Qmax⁡Q_{\max}
  4. 可扩张部分最大线程数与核心线程数之差,代表高峰时可临时启用的线程规模。
    Tmax⁡−TcoreT_{\max} - T_{\text{core}}

任务队列机制

等位区是饭店服务能力的缓冲带。如果没有等位区,客流稍微一多,顾客就直接扭头走了;等位区太大,顾客等太久反而差评。线程池的任务队列就是这个等位区,也是线程池设计中“温和”与“激进”的分水岭。

队列分为有界和无界两大类。LinkedBlockingQueue(默认无界)像一条看不到尽头的走廊,理论上能站无限多人;ArrayBlockingQueue 有固定容量,站满就拒绝新客;SynchronousQueue 则完全没有缓冲,好比“来了就得立刻上桌”,否则就请走;PriorityBlockingQueue 按优先级排序,让 VIP 客人(紧急任务)先插队。每种队列对应一种业务心态:无界是“我全都要”,有界是“量力而行”。

无界队列看起来安全,其实暗藏杀机:当任务生产速度超过处理速度,等位区会无限膨胀,最终把内存撑爆,整个服务 OOM——这比拒客严重得多,饭店走廊挤塌了房子。更反直觉的是,如果用无界队列,maximumPoolSize 几乎永远派不上用场,因为任务永远在排队,永远不会触发“招临时工”的逻辑。

所以队列设计的第一原则是:判断你的系统是“流量平稳”还是“突发脉冲”,再决定有界还是无界。宁可拒绝一部分请求,也不要让队列把内存撑爆——这是线程池设计的必修课。

λ<μ\lambda < \mu
这个不等式是排队论的“稳定条件”:只有当任务到达速率小于处理速率时,队列才不会无限增长。
  1. 任务到达速率单位时间内新产生的请求数量,好比每秒进店的客人。
    λ\lambda
  2. 任务处理速率单位时间内线程能完成的请求数量,好比厨师团队每秒能出的菜。
    μ\mu
  3. 稳定条件一旦到达速率超过处理速率,任何队列(无论有界无界)都会走向积压甚至崩溃。
    λ<μ\lambda < \mu

拒绝策略取舍

饭店满客了怎么办?现实中有四种做法:直接告诉顾客“没位子了”(抛异常);让门口保安自己掏钱请顾客去隔壁吃(调用者自己执行);假装没看见客人默默把门关上(静默丢弃);把等得最久的那桌客人请走,给新客人腾位(丢最旧任务)。线程池的四种拒绝策略正好对应这四种场景:

  • AbortPolicy:直接抛出 RejectedExecutionException,让调用方感知失败,最“硬核”。
  • CallerRunsPolicy:不让任务丢掉,而是由提交任务的线程自己执行,相当于“店长亲自下厨”,天然实现背压。
  • DiscardPolicy:悄悄丢弃新任务,最危险——顾客白等了,系统却毫无反馈。
  • DiscardOldestPolicy:丢弃队列中最旧的任务,再尝试提交新任务,适合任务时效性强的场景。

拒绝策略的触发时机非常明确:当线程数已达最大值且任务队列已满时,新提交的任务就会落入拒绝处理器。这一时刻标志着系统已经到达设计容量的极限,相当于饭店挂出了“客满”的牌子。

选哪种拒绝策略,取决于你的业务能不能接受“丢单”。日志、监控类任务丢了也就丢了,选 DiscardPolicy 无伤大雅;支付、下单类任务必须让调用方知道失败以便重试,选 AbortPolicy 或 CallerRunsPolicy 才稳妥。

reject  ⟺  nactive=Tmax⁡∧Qfull\text{reject} \iff n_{\text{active}} = T_{\max} \land Q_{\text{full}}
这个逻辑公式说明拒绝不是随意发生的,而是“线程满 + 队列满”两个条件同时成立时的必然结果。
  1. 活跃线程数当前正在执行任务的线程数量,包含核心线程和非核心线程。
    nactiven_{\text{active}}
  2. 线程达到上限活跃线程数已经等于最大线程数,所有临时工都已上岗。
    nactive=Tmax⁡n_{\text{active}} = T_{\max}
  3. 队列已满等位区已经没有空位,新到任务无处安放。
    QfullQ_{\text{full}}
  4. 触发拒绝两个条件同时满足时,任务进入拒绝策略处理器。
    reject\text{reject}

线程工厂定制

给厨师发工牌,是饭店管理的基本功。没有工牌,出了食品安全问题你都不知道是哪位师傅经手的。线程工厂(ThreadFactory)就是给线程“发工牌”的地方:统一命名、设置守护状态、配置优先级。

默认的线程名形如 pool-1-thread-1,排障时看到这样的名字,你只知道“有个线程在跑”,却不知道它属于哪个业务。通过自定义线程工厂,可以把线程命名为 ai-svc-embedding-3 或者 order-service-pay-2,一旦线上出现线程死锁或 CPU 飙高,线程转储(thread dump)里的名字能直接告诉你问题出在哪个服务、哪类任务上——排查时间从几小时缩短到几分钟。

线程工厂还能控制守护线程。守护线程就像饭店里“非编制”的杂工,主进程结束它就跟着消失;非守护线程则像正式员工,即使主流程结束也要把手上的活干完。批量任务场景下,错误的守护线程设置可能导致任务被突然中断,数据只写了一半。优先级也是同理:乱设优先级会把系统调度搅乱,通常保持默认即可。

工牌虽小,却是线上排障的第一把钥匙。一个规范命名的线程池,能让你在事故发生时第一时间锁定“哪位厨师烧糊了哪道菜”。

name=P-N,Nseq=Nseq+1\text{name} = P \text{-} N, \quad N_{\text{seq}} = N_{\text{seq}} + 1
线程工厂的核心公式就是“业务前缀 + 递增序号”,它让每个线程都有一个可读的身份。
  1. 业务前缀标识线程归属的字符串,如“订单支付”或“向量检索”。
    PP
  2. 递增序号线程池内部维护的计数器,每次创建线程时自增,保证线程名唯一。
    NseqN_{\text{seq}}

动态调优秘诀

饭店的厨师数量不该一成不变——周末多排班、平日少排班,生意才好。线程池调优同理,核心问题只有一个:核心线程数设为多少合适? 答案取决于任务的性质。

CPU 密集型任务(如复杂计算、模型推理)几乎全程占用 CPU,线程数设太多反而因上下文切换而变慢,经验公式是 CPU 核数 + 1——多出来的一个线程用于弥补偶尔的页面缺失或系统停顿。IO 密集型任务(如网络请求、数据库读写)大量时间在等待 IO,CPU 是闲着的,所以线程数可以远超核数,参考公式是 CPU 核数 × (1 + 等待时间/计算时间)。如果一次请求 90% 时间在等数据库返回,四核机器配 20 个线程也不会让 CPU 过载。

但公式只是起点,压测才是终点。调优的正确姿势是“压测 → 观察 CPU 利用率与任务积压 → 微调线程数 → 再压测”的闭环。现代线程池 API 普遍支持运行期动态修改参数(如 Java 的 setCorePoolSize),这相当于饭店在营业中也能临时增减人手,不必关门调整。

记住一个直觉:CPU 密集是“人多手杂”,IO 密集是“人海战术”。根据任务画像选择公式,再让压测数据说话,线程池就能像饭店排班一样贴合真实客流。

Nthreads=NCPU×(1+WC)N_{\text{threads}} = N_{\text{CPU}} \times \left(1 + \frac{W}{C}\right)
这是 IO 密集型任务的核心线程数估算公式:等待时间占比越高,可以养的线程就越多。
  1. CPU 核心数机器的物理核心数(考虑超线程时以系统识别为准),决定并行计算的天花板。
    NCPUN_{\text{CPU}}
  2. IO 等待时间一次任务中等待外部资源(网络、磁盘、数据库)返回的平均耗时。
    WW
  3. 计算时间一次任务中真正占用 CPU 进行计算的平均耗时。
    CC
  4. 等待计算比W 除以 C 的比值,数值越大说明任务越“闲”,越值得多开线程去填满等待间隙。
    WC\frac{W}{C}

监控预警体系

开店最怕“黑箱经营”——不知道翻台率、不知道客人等待时长,等发现问题时店已经垮了。线程池必须配套监控预警体系,核心指标包括:活跃线程数、队列积压量、任务执行耗时、拒绝次数。活跃线程数接近最大值且队列持续增长,就像饭店门口排起长龙,说明容量即将触顶;拒绝次数突然增多,说明系统正在“赶客”,必须立即扩容或限流。

借助钩子方法(beforeExecute / afterExecute / terminated)实现精确观测:每个任务执行前记录起始时间,执行后计算耗时并上报监控系统。这相当于给每道菜记录“出菜时间”,一旦某类任务特别慢,立刻就能定位到具体业务。

线程池本身也有生命周期状态机:RUNNING(正常营业)、SHUTDOWN(不再接新客,但把已上桌的菜做完)、STOP(立刻停火,甚至打断做了一半的菜)、TIDYING(打扫卫生)、TERMINATED(正式关门)。正确理解状态流转,才能在优雅停机时不丢数据。

监控的本质是“让系统可见”。一个配齐了指标、钩子和状态管理的线程池,就像一家有日报、有台账、有应急预案的饭店——即使遇到高峰,店长也能从容调度,确保每一道菜都稳稳上桌。

RUNNING→SHUTDOWN→TIDYING→TERMINATED\text{RUNNING} \rightarrow \text{SHUTDOWN} \rightarrow \text{TIDYING} \rightarrow \text{TERMINATED}
这是线程池的标准关闭路径:先谢绝新客,再等存量任务完成,最后释放全部资源。
  1. 运行状态线程池正常运行,接受新任务并处理队列中的存量任务。
    RUNNING\text{RUNNING}
  2. 关闭状态不再接受新任务,但已提交和已排队的任务会继续执行完毕,相当于“只出不进”。
    SHUTDOWN\text{SHUTDOWN}
  3. 整理状态所有任务执行完毕,线程池正在执行清理钩子,准备释放资源。
    TIDYING\text{TIDYING}
  4. 终止状态线程池完全停止,所有线程已销毁,资源已归还操作系统。
    TERMINATED\text{TERMINATED}

AI 应用场景

AI推理服务的请求并发控制

阶段:模型服务部署与推理优化

解决的问题:大模型或 CV 模型服务通常依赖 GPU 计算,而 GPU 的显存和算力有限。如果对每个 HTTP 请求都新建线程,高并发下线程数飙升,GPU 任务排队混乱,甚至 OOM 崩溃。AI 厂商需要一个既能充分利用 GPU、又能优雅排队和丢弃超载请求的机制。

具体案例:某智能客服平台的意图识别服务,上线时用 ThreadPoolExecutor(核心线程=2*GPU数, 队列=有界200, 拒绝=CallerRunsPolicy) 控制推理请求。高峰期每秒涌入 800 个请求,线程池先消化 80%,剩下 20% 进入有界队列排队;队列满后由调用方线程自己执行,天然实现背压,GPU 利用率稳定在 85% 以上,服务不再 OOM。

大模型训练数据预处理管道

阶段:数据工程与特征管道

解决的问题:训练大模型前需要抓取网页、下载图片、解析清洗、向量化存储。单线程逐条处理十万级文档需要数天;而无限多线程又会让下游数据库连接池被打爆。此时需要一个可动态调优的线程池来平衡吞吐与下游压力。

具体案例:某 LLM 初创公司的数据管道使用 BoundedThreadPoolExecutor,核心线程 8、最大 32、有界队列 500。每个 worker 从消息队列取到 URL 后并发下载并清洗,超过队列容量时任务留在上游 MQ 中等待重试,绝不丢失数据。压测显示吞吐提升了 9 倍,且下游 ES 集群没有一次连接超时。

分布式追踪日志的高效采集

阶段:可观测性与线上巡检

解决的问题:AI 服务在微服务架构中会生成海量调用日志,如果同步写磁盘,主流程被 IO 拖慢;如果每日志一条线程,线程开销反而超过日志本身。线程池能提供一个“异步缓冲”层,把日志写入与业务逻辑解耦。

具体案例:某推荐系统用单例线程池统一承接所有微服务的 trace 日志,核心线程 4、队列 10000、拒绝策略 DiscardPolicy——日志偶尔丢几条可以接受,但绝不能影响主流程。队列积压量作为监控指标接入告警,一旦接近上限就自动提升日志采样率的下限,保障了核心链路 P99 稳定在 120ms 内。

Python 演示

python44 行
import threadingfrom concurrent.futures import ThreadPoolExecutor  class BoundedThreadPool:    def __init__(self, max_workers, max_queued):        self.pool = ThreadPoolExecutor(            max_workers=max_workers, thread_name_prefix="ai-svc"        )        self.slots = threading.Semaphore(max_queued + max_workers)        self.rejected_count = 0     def submit(self, fn, *args, **kwargs):        if not self.slots.acquire(blocking=False):            self.rejected_count += 1            return False        def _runner():            try:                fn(*args, **kwargs)            finally:                self.slots.release()        self.pool.submit(_runner)        return True     def shutdown(self):        self.pool.shutdown(wait=True)  def mock_llm_call(task_id):    threading.Event().wait(0.05)    print("handled", task_id)  def main():    pool = BoundedThreadPool(max_workers=2, max_queued=3)    for i in range(10):        ok = pool.submit(mock_llm_call, i)        print("task", i, "accepted" if ok else "rejected")    pool.shutdown()    print("rejected_total", pool.rejected_count)  if __name__ == "__main__":    main()
目的:实现一个带“有界队列 + 拒绝计数”的线程池,模拟任务超过容量时被拒绝的行为,与正文中“队列满 + 线程满 → 拒绝”的原理一一对应。
  1. ThreadPoolExecutor(max_workers=2, thread_name_prefix="ai-svc")创建线程池,cli 参数给线程命名加业务前缀,方便排障。
  2. self.slots = threading.Semaphore(max_queued + max_workers)用信号量作为“等位区 + 工位”的总容量闸门。
  3. self.slots.acquire(blocking=False)非阻塞获取名额,拿不到就立刻拒绝任务,队列满不等待。
  4. self.rejected_count += 1拒绝次数留痕,对应正文的监控预警指标。
  5. self.pool.submit(_runner)真正的任务进入底层线程池执行,_runner 负责释放名额。
  6. pool.shutdown(wait=True)优雅关闭,等存量任务跑完再退出,对应状态机中的 SHUTDOWN。

常见误区

误区我设置的线程数越多,系统处理能力就越强。线上并发了 5000 个请求,我就把最大线程数调到 5000,这样所有请求都能“同时”处理。

正解线程不是免费的劳动力。当活跃线程数超过 CPU 核心数时,每个线程都要分时间片轮转,频繁的上下文切换会让 CPU 大量时间花在“存现场、换人”上,实际吞吐反而下降。就像饭店招了 100 个厨师挤在 10 平米的后厨——人越多越容易互相撞肘,出菜反而更慢。线程数应该根据 CPU/IO 密集程度和实际压测结果决定。

为何常见:“人多好办事”是日常经验直觉,但忽视了计算机中上下文切换和资源竞争的本质成本。

误区任务队列设置成无界就很放心,因为不会丢任务,内存大不了多占一点,反正任务总会处理的。

正解无界队列意味着队列可以无限增长。当任务生产速度超过消费速度,积压任务会持续占据内存直到撑爆 JVM(OOM),整个服务宕机,比丢几个任务严重得多。且无界队列使最大线程数参数完全失效——任务永远在排队,永远触发不了扩容逻辑。正确做法是用有界队列并配套监控队列积压量。

为何常见:从“不丢数据”的业务直觉出发,而忽略了内存是有限资源,队列无限增长本质上是“缓缓崩溃”。

误区我机器是 8 核,所以线程池核心线程数就该设为 8,或者 9。

正解这个结论只适用于纯 CPU 密集型任务。对于 IO 密集型任务(网络请求、数据库读写、GPU 等待),大部分时间线程都在阻塞等待,CPU 是闲置的,此时应该用 N = N_CPU × (1 + W/C) 的公式放大线程数,让更多任务在等待间隙“插队”占用 CPU。设成 8 反而浪费了 IO 等待期间的算力。

为何常见:把“核数 = 并行度”的直觉泛化到了所有任务类型,忽略了任务中等待与计算的占比差异。

误区调用 `shutdown()` 之后,线程池里的线程马上就销毁了,正在跑的任务也会被中断,所以我不敢调用 shutdown。

正解shutdown() 的逻辑是“不再接受新任务,但队列中已有的任务会继续执行直到完成”,然后才进入终止状态。如果要立刻中断所有任务应该用 shutdownNow()(Java)。Python 的 shutdown(wait=True) 会阻塞到全部任务完成。优雅关闭是有状态的、温和的过程,正如饭店打烊是先谢客、再收尾、最后关灯。

为何常见:把“关闭”理解成了“断电”,而优雅关闭的设计意图正是“完成存量、拒绝增量”。

面试题

Q1. ThreadPoolExecutor 处理新任务的正确顺序是?

单选题 · easy

  • A.创建线程 → 入队 → 拒绝
  • B.核心线程 → 任务队列 → 非核心线程 → 拒绝← 正确答案
  • C.任务队列 → 核心线程 → 非核心线程 → 拒绝
  • D.非核心线程 → 核心线程 → 任务队列 → 拒绝

答案:B。如同饭店先找空闲厨师(核心线程),没厨师让客人等位(队列),等位满了才招临时工(非核心线程),最后才拒客。其他选项颠倒了顺序,不符合线程池的“先复用、再排队、后扩容”设计。

Q2. 核心线程数 2、最大线程数 4、有界队列容量 2,当前有 4 个任务正在执行、队列已满。第 5 个任务提交时会发生什么?

单选题 · mid

  • A.直接进入队列执行
  • B.立刻创建非核心线程执行
  • C.被拒绝策略处理← 正确答案
  • D.等待某个线程空闲再执行

答案:C。此时活跃线程 4 = 最大线程数,且队列已满,两个饱和条件同时成立,任务必然落入拒绝策略。如果活跃线程数未达上限,则优先扩容线程而不是无限入队——这正是“等位区满之后才招临时工”的规则。

Q3. 关于无界任务队列,下列说法正确的是?

单选题 · mid

  • A.不会丢失任务且绝对安全
  • B.使最大线程数参数失去作用← 正确答案
  • C.适合所有高并发场景
  • D.队列满了会自动扩容

答案:B。无界队列永远不会“满”,所以永远触发不了“创建非核心线程”的分支,最大线程数形同虚设。它还会在积压时撑爆内存。饭馆走廊设成无限长看似不拒客,走廊尽头连墙都没有,客人全挤在黑洞里。

Q4. 以下哪些是 ThreadPoolExecutor 内置的拒绝策略?

多选题 · mid

  • A.AbortPolicy 直接抛异常← 正确答案
  • B.CallerRunsPolicy 调用者执行← 正确答案
  • C.RetryPolicy 自动重试
  • D.DiscardOldestPolicy 丢最旧← 正确答案

答案:ABD。四种内置策略是 Abort、CallerRuns、Discard、DiscardOldest。RetryPolicy 并不是 JDK 内置策略,自动重试需要自己包装任务实现。就像饭店满客只有“请走、店长亲自下厨、假装没看见、请走最旧客人”四种玩法。

Q5. keepAliveTime 参数主要作用于哪类线程?

单选题 · easy

  • A.核心线程
  • B.非核心线程← 正确答案
  • C.所有任务队列
  • D.拒绝策略处理器

答案:B。空闲存活时间默认只对超过核心线程数的非核心线程生效,临时厨师闲了太久就会被辞退。如果设置了 allowCoreThreadTimeOut(true),核心线程也能被回收,相当于正式厨师淡季也可以放假。

Q6. 8 核 CPU 机器上运行纯 IO 密集型任务,每次请求等待 IO 耗时 90ms、计算耗时 10ms,合理的核心线程数接近?

单选题 · hard

  • A.9
  • B.16
  • C.40
  • D.80← 正确答案

答案:D。IO 密集公式 N = 8 × (1 + 90/10) = 80。每线程只有 10% 时间占用 CPU,80 个线程同时运行也才约等于 8 核满载。选 9 是 CPU 密集型的答案,选 16 和 40 则没有结合等待计算比来估算。

Q7. 线程池处于 SHUTDOWN 状态时,以下哪些说法正确?

多选题 · hard

  • A.不再接收新任务← 正确答案
  • B.已提交任务会继续执行← 正确答案
  • C.立刻中断正在运行的任务
  • D.队列中的任务会被清空丢弃

答案:AB。SHUTDOWN 是温和关闭:新任务进不来,但已提交的存量任务会执行完成。立刻中断是 STOP 状态的行为。就像饭店挂出“打烊”牌子,还在吃的客人可以吃完,但不再放新客进店。

Q8. 关于自定义线程工厂,下列哪项是它的典型作用?

单选题 · single

  • A.设置线程业务前缀便于排查← 正确答案
  • B.控制任务队列的长度
  • C.决定拒绝策略的类型
  • D.调整核心线程池大小

答案:A。线程工厂负责创建线程并设置名称、守护状态、优先级,核心价值是给线程起一个“能看懂的名字”,如 ai-svc-embedding-3。队列长度、拒绝策略、池大小都是构造参数或策略对象的事,不属于工厂职责。

Q9. 使用线程池相比“每任务新建线程”有哪些收益?

单选题 · multiple

  • A.降低线程创建销毁开销← 正确答案
  • B.限制系统并发水位← 正确答案
  • C.保证任务一定能按时间完成
  • D.提供队列缓冲抗流量冲击← 正确答案

答案:ABD。池化复用了线程,避免反复创建销毁;有界队列和最大线程数限制了并发水位;队列天然提供了缓冲。但线程池不保证任务按时完成,执行时间取决于任务本身的耗时和调度。

Q10. 动态调优线程池时,下列哪项是“压测 → 调整”闭环的正确观察指标?

单选题 · mid

  • A.线程名的长度
  • B.队列积压量和 CPU 利用率← 正确答案
  • C.拒绝策略的类名
  • D.线程工厂的优先级设置

答案:B。调优要看系统真实水位:队列积压反映供需差距,CPU 利用率反映资源是否打满。线程名、策略类名、优先级都只是配置信息,不是运行状态的反馈信号。店长排班要看翻台率,而不是看菜单名字好不好听。