4. 并发与线程参数:最大线程数、线程池大小、任务队列深度
聊到并发和线程,很多人的第一反应就是「线程越多越快」。嗯,这个想法我早年也深信不疑。直到有一次线上事故,线程数一上去,系统直接挂了——不是 CPU 跑满,而是内存爆了,上下文切换把 CPU 时间全吃掉了。
说白了,这三个参数就是一套流量控制阀门。调得好,系统稳如老狗;调不好,再好的硬件也白搭。
4.1 最大线程数(Max Threads)
最大线程数,就是系统允许同时运行的最大线程数量。这个值不是越大越好,我见过太多人一上来就设 1000、2000,结果呢?
- CPU 密集型任务:线程数超过 CPU 核心数后,性能反而下降。因为上下文切换的开销超过了并行计算带来的收益。
- IO 密集型任务:可以适当多开线程,因为线程在等待 IO 时会让出 CPU。但也不是无限制,内存和文件句柄都是瓶颈。
经验公式:
- CPU 密集型:Max Threads ≈ CPU 核心数 + 1
- IO 密集型:Max Threads ≈ CPU 核心数 × (1 + 等待时间 / 计算时间)
我记得有一次帮客户调优一个 Web 服务。他们设了 500 个最大线程,结果平均响应时间 3 秒。我压到 200 个线程,响应时间降到 800 毫秒。为什么?因为线程太多,锁竞争和上下文切换把系统拖垮了。
4.2 线程池大小(Thread Pool Size)
线程池大小和最大线程数不一样。线程池是预先创建好的一组线程,任务来了直接复用,省去创建和销毁的开销。
我个人习惯把线程池大小分成三档:
| 场景 | 核心线程数 | 最大线程数 | 说明 |
|---|---|---|---|
| CPU 密集型 | N + 1 | N + 1 | N 为 CPU 核心数 |
| IO 密集型 | 2N | 4N | 等待时间占比高 |
| 混合型 | N × (1 + W/C) | 2 × 核心数 | W=等待时间,C=计算时间 |
小技巧:线程池大小不是一成不变的。我建议在压测环境下用 jstack 抓线程状态,看看有多少线程在 RUNNABLE,多少在 WAITING。如果 WAITING 占比超过 70%,说明线程池太小了。
4.3 任务队列深度(Queue Depth)
任务队列深度,就是线程池满了之后,新任务排队等待的长度。这个参数最容易被忽略,但坑也最多。
我曾经遇到过一个案例:队列深度设了 10000,结果高峰期任务堆积,内存直接飙到 2GB。等队列里的任务开始执行时,数据已经过期了——用户早就不耐烦关掉了页面。
避坑指南:
- 队列深度不要设太大,否则会成为内存泄漏的温床
- 建议配合拒绝策略使用:队列满了直接拒绝,比让用户无限等待要好
- 实时性要求高的场景,队列深度建议 ≤ 100
4.4 三者之间的关系
这三个参数不是孤立的。你想想看:
- 线程池大小 + 队列深度 = 系统能承受的最大并发任务数
- 最大线程数 ≥ 线程池大小(否则线程池设了也白设)
- 队列深度 × 平均任务耗时 = 任务在队列里的等待时间
我一般这样调优:先确定可接受的响应时间,然后反推队列深度。比如响应时间不能超过 500ms,平均任务耗时 50ms,那队列深度就不能超过 10。超过 10 的任务,直接拒绝或者降级处理。
4.5 实战调优步骤
我总结了一套调优流程,你可以直接拿去用:
- 确定业务指标:响应时间、吞吐量、资源利用率
- 压测基线:用默认参数跑一轮,记录 CPU、内存、线程状态
- 调整线程池大小:按公式计算初始值,逐步增加,观察拐点
- 调整队列深度:根据响应时间要求反推,一般从 100 开始
- 设置拒绝策略:推荐
CallerRunsPolicy或自定义降级 - 验证:用生产流量回放或压测工具验证
我常用的工具:
jstack:看线程状态分布jstat:看 GC 频率和内存VisualVM:图形化监控线程池JMeter:压测工具,模拟并发
嗯,最后说一句:这三个参数没有银弹。不同的业务、不同的硬件、不同的负载模型,最优值都不一样。我的建议是——先压测,再调优,最后上生产。别像我当年那样,拍脑袋设个 1000,结果线上炸了才后悔。