2. 任务与线程的底层关系

线程池之所以能提升吞吐,并不是因为它“创建线程更快”,而是因为它把任务与线程这两层解耦了:任务描述“要做什么”,线程描述“由谁来做”。理解这两层的映射关系,是后续调参、排障和容量估算的基础。

一、任务抽象:从 Runnable 到可调度单元

在 Java 中,提交给线程池的最小单元是 Runnable 或 Callable<V>。它们本身只是“一段逻辑 + 可能的返回值”,并不携带线程信息。线程池在 execute() 或 submit() 时,会把任务包装成内部结构(如 FutureTask、Worker 持有的 firstTask),再交给某个工作线程执行。

这个包装过程带来三个工程含义:

  • 任务是有状态的:新建、排队中、执行中、已完成、已取消。线程池的队列长度、拒绝策略都作用在“任务状态”上,而不是线程上。
  • 任务与线程是多对一:一个线程在其生命周期内会执行成百上千个任务,因此任务里不能假设“线程是我独占的”。
  • 任务不应持有线程局部假设:如果任务依赖 ThreadLocal 且未清理,下一个任务复用同一线程时会读到脏数据。

一个常见的教学示例:提交 1000 个计算任务,核心线程 4、最大线程 8、队列容量 100。前 4 个任务直接由核心线程执行,第 5~104 个进入队列,第 105 个开始才创建非核心线程直到 8 个,第 109 个之后触发拒绝策略。这里被调度的对象始终是“任务”,线程只是执行载体。

二、线程生命周期与池化后的变化

原生线程的生命周期是 NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED。进入线程池后,线程被复用,TERMINATED 不再是常态,取而代之的是池内的“空闲—取任务—执行—再空闲”循环。工作线程通常阻塞在 getTask() 上等待队列任务,此时处于 WAITING 或 TIMED_WAITING。

需要特别区分两种“等待”:

  • 空闲等待:线程在队列上阻塞,不消耗 CPU,属于健康状态。
  • 业务阻塞:任务内部等待 I/O、锁或下游响应,线程被占用但不出活,是吞吐下降的主因。

线程池的 allowCoreThreadTimeOut、keepAliveTime 控制的是空闲线程的回收,对业务阻塞无效。因此“线程数够不够”不能只看池大小,要看线程处于哪种等待。

三、上下文切换成本:被低估的隐性开销

上下文切换是 CPU 从一个线程切换到另一个线程时保存/恢复寄存器、程序计数器、栈指针等状态的过程。它本身不执行业务,却消耗 CPU 时间,并污染 CPU 缓存。工程上可参考的量级:一次轻量切换约 1~5 微秒,涉及跨核迁移或缓存失效时可达 10 微秒以上。若线程数远超 CPU 核数,切换次数会随并发线程数上升而放大。

可以用一个简化估算判断是否“线程过多”:设单任务计算耗时 C、阻塞耗时 B,CPU 核数 N。若线程数 P 远大于 N×(1+B/C),则大量线程只是在争抢 CPU 时间片,切换开销占比升高,吞吐不再增长甚至下降。这也是“CPU 密集型线程数≈核数、I/O 密集型可适当放大”这一经验公式的来源。

四、并发与并行的工程边界

并发是任务在时间上交替推进的能力,并行是任务在同一时刻真正同时执行。单核也能并发,但无法并行。线程池的价值在于:用有限的并行资源(核数)支撑远大于核数的并发任务量。

工程边界由此确定:

  • 并行度上限由 CPU 核数与任务中不可并行部分决定(受 Amdahl 定律约束)。
  • 并发度上限由内存、连接数、下游承载能力和队列容量共同决定。
  • 线程数只是“并发度的实现手段之一”,不是越大越好。
判断线程池是否健康,优先看三个信号:活跃线程数是否长期贴近最大值、队列是否持续积压、任务平均等待时间是否随并发上升而恶化。三者同时出现,通常说明并行资源已被打满,继续加线程只会增加切换成本。
任务抽象 → 线程执行 → 上下文切换 → 并行边界 任务层(Task) Runnable / Callable FutureTask 包装 状态:排队/执行/完成 多对一映射,不独占线程 提交 线程层(Worker) 核心线程 / 非核心线程 getTask() 阻塞取任务 复用:空闲↔执行循环 keepAliveTime 控制回收 调度 CPU 执行 时间片轮转 上下文切换 1~5μs 缓存失效 / 跨核迁移 并行度 ≤ 核数 并发(Concurrency) 任务交替推进,单核也可实现 并发度上限:内存 / 连接 / 队列 / 下游 线程数只是实现手段之一 P ≫ N×(1+B/C) → 切换开销上升 C=计算耗时,B=阻塞耗时,N=核数,P=线程数 并行(Parallelism) 同一时刻真正同时执行 并行度上限:CPU 核数 受 Amdahl 定律约束 CPU 密集:线程数 ≈ 核数 I/O 密集:可适当放大,但需验证

五、把关系落到工程判断上

综合来看,任务与线程的关系可以归纳为一条判断链:先看任务类型(CPU 密集还是 I/O 密集),再看线程状态(空闲等待还是业务阻塞),最后看并行资源是否被打满。若活跃线程数长期等于最大值且队列积压,说明并行资源不足,此时应优先优化任务本身(减少阻塞、拆分大任务),而不是盲目调大线程数。

一个可参考的配置示例(教学假设,非实测):8 核机器,任务平均计算 2ms、阻塞 8ms,则 1+B/C = 5,理论线程数约 8×5 = 40。可先设核心 16、最大 40、队列 200,观察活跃线程与队列变化,再逐步收敛。若切换开销明显(可通过 vmstat 的 cs 列或 pidstat -w 观察),则应下调线程数或改用异步非阻塞模型。

不要把“线程数 = 最大并发数”当成等价关系。线程数只是并行执行体的数量,真正的并发能力还受队列、下游、锁竞争和内存限制。线程池调参的本质,是在并行资源、切换成本和任务等待时间之间找平衡点。