Java线程池参数优化实战:从事故复盘到生产配置指南
1. 那场让我彻底重新审视线程池的生产事故1.1 事故现场从一条异常日志说起先讲一个真实经历。去年某个版本迭代后线上一个核心交易服务在晚高峰突然出现大面积超时监控面板上TP99从正常的80ms直接飙到3000msCPU使用率从35%冲上95%紧接着一堆业务告警炸了锅。登录服务器看日志发现大量这样的堆栈java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask7c3f3b2e rejected from java.util.concurrent.ThreadPoolExecutor6d2f7b3a[Running, pool size 200, active threads 200, queued tasks 10000, completed tasks 123456]看到queued tasks 10000和pool size 200这两个数字我大概就猜到问题出在哪了。这个服务的线程池配置是典型的拍脑袋三部曲核心线程数填200最大线程数填200队列容量填10000。看起来很稳实际上一到流量波峰就是灾难。因为我当时用的是newFixedThreadPool(200)底层实现就是ThreadPoolExecutor(200, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueueRunnable())。这个队列是无界的那10000是因为我后来自己改成了有界队列。无界队列的模式下任务只进不出直到内存被彻底打爆。1.2 事故根因线程池被饿死的完整链条梳理一下当天的故障链路其实是一个很经典的连锁反应上游依赖的一个商品服务接口在某次发布后TP99从40ms恶化到800ms导致我这个服务的调用线程大量阻塞在等待上游响应上。核心线程200个很快全部占满线程数达到maximumPoolSize。新任务进入LinkedBlockingQueue排队队列无界所以任务只增不减。业务请求持续涌入队列里积压的任务越来越多每个任务持有的内存引用、数据库连接、文件句柄都得不到释放。最终某个瞬间OutOfMemoryError: Java heap space直接让服务宕机。等我把服务拉起来由于下游还在慢同样的路径又走了一遍这次连Insufficient memory都来了整个JVM直接起不来。那段时间我为了查问题把线上线程池的运行数据导出来看active threads一直是200queued tasks从几百涨到几万而真正在做计算的任务连10%都不到绝大多数线程都卡在远程调用上。看这个数据时我才真正明白一件事——线程池参数的坑不是参数公式背不背得出来而是你有没有理解每个参数在真实流量下的行为。1.3 复盘后我建立的三个认知事故复盘之后我给自己立了三条规矩这些在后面的内容里会反复提到绝对不用无界队列。无界队列看似解决问题实则是把内存溢出的风险从线程池内部转移到了堆内存上该炸还是炸只是炸的方式更难看。线程数不是越大越好。线程是宝贵的资源每个线程默认栈大小1MB64位JVM下通常是1MB200个线程光栈就要占200MB更不用说线程切换的CPU开销。线程池必须可监控、可动态调整。静态配置的线程池在生产流量面前就是定时炸弹你不知道它什么时候会顶不住。这篇文章就是把我在这些事故中反复验证过的线程池参数优化方法和踩坑经验系统整理出来希望能帮你避开这些坑。2. 线程池七个参数别再背八股文要理解设计意图2.1 corePoolSize核心线程数到底该怎么定面试八股文里最常问的七个参数背出来很简单corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler。但面试官真正想听的是你怎么确定这些值。corePoolSize是最容易被拍脑袋的参数。很多人喜欢把核心线程数设置为CPU核数×2、CPU核数1这些公式在纯计算型任务上有些道理但对于一个80%时间都在等待IO响应的业务服务完全是误导。我的经验是分两类场景看CPU密集型任务计算、加密、压缩等核心线程数建议设置为CPU核数 1这个 1 是为了防止某个线程因缺页中断或其他原因暂停时CPU空转。IO密集型任务远程调用、数据库访问、文件读写等线程大部分时间在等待可以设置更多线程。经验公式是CPU核数 × (1 平均等待时间 / 平均计算时间)但实际工作中很难精确计算这两个时间我更推荐用压测来校准。举个例子我之前做过一个网关服务主要逻辑是解析请求、调用下游两个HTTP接口、组装响应。用8核的机器压测发现线程数从64涨到128时吞吐量提升了约40%但从128涨到256时吞吐量几乎不再变化反而CPU上下文切换次数明显上升。最终核心线程数定在128这个值远大于81或者8×2因为等待时间确实占了绝大部分。压测校准的方法也很简单从一个小值开始比如32每轮压测增加32个线程观察吞吐量和TP99的拐点。拐点出现前的那个值就是最合适的线程数。2.2 maximumPoolSize 与 workQueue不是两个独立参数是一套组合拳maximumPoolSize和workQueue是协同设计的这俩分开看很容易出问题。我见过很多团队把corePoolSize20、maximumPoolSize200、workQueue容量设置为10000这样的配置意味着什么意味着当20个核心线程全部繁忙时新任务全部进队列排队maximumPoolSize扩容逻辑根本不会触发直到队列满了才会扩容到200。这就是一个典型误区队列容量设得太大maximumPoolSize形同虚设队列容量设得太小拒绝策略频繁触发。合理的配置应该是让队列在正常情况下刚好能承接流量的小高峰而不是把所有积压任务全部装进队列。我个人的设计原则是队列容量根据可容忍的排队时间倒推。比如你希望任务在队列中最长等待500ms单个任务平均执行时间50ms那么队列容量可以设置为高峰期每秒任务数 × 0.5左右。maximumPoolSize存在的意义是应对短时突发流量所以它应该比corePoolSize大一些但一定不是越大越好。线程数过大时JVM的线程调度开销和线程争用会急剧上升。这里要提一下ThreadPoolExecutor的一个重要行为提交任务时是先判断核心线程是否已满再判断队列是否已满最后才判断是否达到最大线程数。也就是说线程数从core扩容到max的过程中新任务先进队列而不是直接开新线程。很多人设计时没搞清楚这一层配置出来的线程池和预期行为南辕北辙。2.3 另外四个参数细节里藏着你看不见的坑keepAliveTime和unit是控制非核心线程超过corePoolSize部分的线程空闲存活时间的。我见过不少配置把这个时间设置成几秒甚至零结果是流量稍微一波动线程数就在core和max之间反复横跳频繁创建和销毁线程的开销比省下的资源还大。生产环境我一般设置为60秒或120秒让线程在突发流量过后多存活一会儿避免抖动的流量把线程池折腾得够呛。threadFactory是被忽视最多的一个参数。不用自定义ThreadFactory的后果是当你用jstack查看线程栈时看到的全是pool-3-thread-1这种名字根本分不清是哪个业务线程池。有一次排查线上问题我费了很大力气才从二十多个pool-x-thread-y中找到目标线程。自定义ThreadFactory是几十行代码的事却能在排查问题时省下数小时ThreadFactory namedThreadFactory new ThreadFactory() { private final AtomicInteger counter new AtomicInteger(0); Override public Thread newThread(Runnable r) { Thread t new Thread(r, biz-order-pool- counter.incrementAndGet()); t.setDaemon(false); t.setPriority(Thread.NORM_PRIORITY); return t; } };handler拒绝策略这个参数后面我会单独用一整节来讲因为它也是生产环境里最容易被配置错的地方。3. 阻塞队列选型与容量设计队列选不对参数全白费3.1 三种常用阻塞队列的真实差异阻塞队列的选择往往决定了整个线程池的行为模式我在生产中用过的队列主要有三种各有各的脾气队列类型是否有界特点常用场景LinkedBlockingQueue默认无界可指定容量链表结构吞吐量高但无界时风险大有界化后用于通用异步任务ArrayBlockingQueue有界数组结构有界且容量固定对队列长度有严格上限的场景SynchronousQueue不存储元素每个插入必须等待一个移除否则阻塞直接提交模式配合较大maxPoolSizeLinkedBlockingQueue默认无界这一点我建议你直接忘掉这个默认值。无界队列最大的问题不是队列无限增长——毕竟内存有限队列增长到一定程度必然OOM——而是它的风险是延迟爆发的。任务堆积时你看到的可能是内存预警而你根本意识不到问题其实出在线程池上。SynchronousQueue是最特殊的一种。它不存任务提交的任务必须直接交给一个空闲线程如果没有空闲线程就尝试创建新线程只要没到maximumPoolSize。这种队列的场景非常有限适合那些希望绝不排队、要么立即执行、要么直接拒绝的任务。比如一些实时性要求极高、队列等待没有意义的场景。很多newCachedThreadPool内部用的就是SynchronousQueue线程空闲60秒后回收。ArrayBlockingQueue和LinkedBlockingQueue在指定有界容量后实际的差别主要在性能。LinkedBlockingQueue用了两把锁分别控制队头和队尾吞吐量通常略好一些ArrayBlockingQueue用一把锁控制全部操作实现上更简单在低并发下表现也很稳定。生产场景两者差别不大我更看重的是语义一致性所以我通常统一用LinkedBlockingQueue因为它的offer、put、poll行为很直观不容易踩坑。3.2 有界队列容量不是填一个数字那么简单队列容量怎么定我见过很多团队填1000、10000全凭感觉。定容量前要回答一个问题队列满时你希望线程池做什么是让更多线程参与处理扩容到maximumPoolSize还是拒绝新任务触发拒绝策略还是让提交线程自己去执行CallerRunsPolicy想清楚这个问题后队列容量就变成了一个预算问题。假设一个服务高峰期每秒接收到500个任务单个任务平均耗时50ms那么正常情况下每个线程每秒能处理20个任务25个线程就能打满500 QPS。你设置核心线程数50已有一定冗余。如果要让任务排队时间不超过1秒队列容量可以设置为500 × 1秒 × (1 / (1 - 负载系数))这种量级负载系数可以取0.7左右。算出来的值可以作为初始值再通过压测微调。我自己常用的简化版本是队列容量 允许排队的最大任务数 高峰期QPS × 允许排队秒数比如高峰期QPS是200允许排队最长3秒队列容量就设为600。注意这个值和线程数是配合的如果线程数已经足够快消费任务队列自然不会被填满如果消费速度跟不上600个任务的等待时间也会自动延长。3.3 JDK内置的四种拒绝策略别只会用AbortPolicy默认的AbortPolicy是直接抛RejectedExecutionException这个策略的原始行为听着简单但坑也在这里——很多团队根本没有catch这个异常。异常抛出后如果你用的是submit()方法提交任务这个异常甚至不会立刻体现在提交方的调用栈上而是被封装到Future里等你调用future.get()时才抛出来。那在线业务上用户请求已经返回了错误日志里却找不到异常堆栈。四种拒绝策略我在生产中都真实踩过AbortPolicy默认抛异常适合明确要求快速失败且调用方有兜底逻辑的场景。用这个策略调用方必须catchRejectedExecutionException并做降级处理。CallerRunsPolicy提交任务的线程自己执行这个任务。这个策略我一定要单独说它是四种策略里最温柔也最容易被误用的。好处是任务不会丢坏处是如果提交方是Tomcat的工作线程它会因为执行任务而阻塞这会反过来对提交方做背压。在高并发场景下这可能导致提交方线程耗尽、整体雪崩。我一般只在内部异步化场景使用对外提供服务的接口不敢用。DiscardPolicy静默丢弃。最危险我强烈建议不要在生产用因为任务丢失毫无感知排查问题时无从下手。DiscardOldestPolicy丢弃队列里最老的任务然后重试提交当前任务。适合能接受丢弃部分非关键任务的场景比如日志上报、埋点数据。我在做系统设计时通常会自定义一个拒绝策略把被拒绝的任务数据记录下来同时触发告警。核心代码大致如下RejectedExecutionHandler rejectHandler (r, executor) - { if (r instanceof FutureTask) { // 记录任务内容、提交时间、队列状态 monitor.recordReject(order-async-pool, executor.getQueue().size()); } throw new RejectedExecutionException(order-async-pool queue full, current queue size executor.getQueue().size()); };这样既保留了快速失败的能力又能通过监控系统及时发现队列打满的风险。4. submit与execute的抉择差之毫厘谬以千里4.1 异常吞噬陷阱Future.get() 与 UncaughtExceptionHandlerexecute(Runnable)和submit(Callable)是线程池提交任务的两种方式表面看只是一个有返回值一个没有但实际差异影响非常大。用execute()提交任务时如果任务内部抛出非受检异常这个异常会传递给线程的UncaughtExceptionHandler。如果不设置自定义的handler默认行为是打印堆栈到stderr然后线程销毁线程池会创建一个新线程补充进来。看起来问题不大但如果你没有设置threadFactory异常信息可能被其他日志冲掉非常难排查。用submit()提交任务时异常被封装进Future对象。这是一个更隐蔽的坑如果任务内部抛了异常你压根不会在这个任务提交的时候感知到异常被吞掉了。直到你调用future.get()才会抛出ExecutionException。但如果你的业务代码根本不关心返回值从不调用get()那异常就真的石沉大海了。我踩过这样一次坑一个数据同步任务用submit()提交任务执行到一半因为某个上游数据格式异常抛了空指针但由于没人调用future.get()这个异常一直没暴露。第二天业务方反馈数据没有更新我排查了很久才发现线程池里3个线程已经悄悄死过一轮了任务中断后靠后续补偿任务才恢复。那之后我给自己定了个规矩凡是submit()提交的任务要么把返回值接收下来做统一异常处理要么在任务内部加try-catch绝对不允许让异常静默发生。4.2 实际项目中我如何选择现在我在代码评审时对submit和execute的选择有一套自己的判断逻辑纯异步通知类任务比如发送消息、日志记录、埋点上报用execute()配合自定义UncaughtExceptionHandler做兜底日志。这类任务不需要返回值也不应该阻塞主流程。需要拿执行结果的任务比如并行调用多个接口汇总数据用submit()但必须对Future设置合理的get(timeout)超时避免某个子任务卡死导致整个主线程阻塞。定时任务或后台批处理用execute()更清晰配合任务级的异常兜底机制保证单个任务失败不影响整个批次。这里还涉及一个面试常问的坑submit()除了传Callable还能传Runnable但Runnable的返回值是null。有时候为了统一接口我会故意用submit(() - doSomething())此时Future.get()返回的null也可能造成空指针隐患需要小心。4.3 自定义UncaughtExceptionHandler为线程池兜底不管用哪种方式提交任务我都建议在ThreadFactory里设置自定义UncaughtExceptionHandler这是最后一个兜底防线。示例ThreadFactory factory r - { Thread t new Thread(r, async-report-pool- counter.incrementAndGet()); t.setUncaughtExceptionHandler((thread, throwable) - { log.error(Thread [{}] caught uncaught exception, thread.getName(), throwable); alertService.send(线程池任务异常: throwable.getMessage()); }); return t; };这个handler只能捕获那些没有被try-catch住的异常。如果一个任务内部已经自己catch了外面的handler永远看不到。但作为最后一道安全网它的价值在于即使代码里有遗漏你至少能在日志和监控中看到异常而不是让问题在黑暗里生长。5. 生产环境线程池配置全流程从压测到监控再到动态调优5.1 不同业务场景的参考参数先给出一组我沉淀下来的参考配置这些值不是标准答案但可以作为一个起步点业务场景corePoolSizemaximumPoolSize队列类型与容量拒绝策略高吞吐异步通知8核8-1632LinkedBlockingQueue(200)自定义告警快速失败计算密集型8核8-99-12LinkedBlockingQueue(100)CallerRunsPolicyIO密集型HTTP调用8核64-128128-256LinkedBlockingQueue(500)自定义告警降级实时性极高的消息处理1632SynchronousQueueAbortPolicy这个表格的核心逻辑是队列不能太长让maximumPoolSize有被触发扩大的空间拒绝策略要配合业务做兜底而不是简单抛异常或丢弃。5.2 压测校准别拿线上当试验场最靠谱的参数来源不是某个公式而是你自己的压测数据。我通常用以下步骤来校准参数确定业务场景的主路径是什么是CPU密集型还是IO密集型。设置一组初始参数可以参考上面的表格部署到预发环境。用压测工具模拟线上流量模型逐步增大并发观察以下几个指标吞吐量每秒处理的任务数TP99 / TP999响应延迟线程池实际活跃线程数通过ThreadPoolExecutor的getActiveCount()监控CPU核数、上下文切换次数找到吞吐量增长的拐点那个并发量对应的线程数就是合理值。有一次我压测一个异步任务线程池8核机器初始配置corePoolSize16, maximumPoolSize64。压测到40并发时吞吐量还在涨到50并发时吞吐量反而下降vmstat显示cs上下文切换从10000飙升到50000。这说明线程数已经超过合理范围最终定在48个线程是最优解。压测能让你看到真实的数据而不是猜一个数。5.3 线程池可视化监控不监控的线程池定时炸弹线程池运行状态必须纳入监控体系。ThreadPoolExecutor提供了一组现成的监控指标getPoolSize()当前线程池的线程数getActiveCount()活跃线程数getQueue().size()排队任务数getCompletedTaskCount()已完成任务总数getRejectedExecutionCount()这是自定义的JDK没有直接提供需要自己包装一个计数器我一般会用ThreadPoolExecutor的beforeExecute、afterExecute和terminated钩子方法来做任务级别的耗时统计。生产上可以设计一个简单的MonitorThreadPoolExecutor子类public class MonitorThreadPoolExecutor extends ThreadPoolExecutor { private final LongAdder rejectedCount new LongAdder(); public MonitorThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler); } Override protected void beforeExecute(Thread t, Runnable r) { // 记录任务开始时间到ThreadLocal或Map结构 } Override protected void afterExecute(Runnable r, Throwable t) { // 计算任务执行耗时上报到监控系统 if (t ! null) { // 处理任务执行异常 } } Override public void reject(Runnable r) { rejectedCount.increment(); super.reject(r); } }将这些指标定时上报到Prometheus、Graphite或公司的内部监控系统配置告警规则队列使用率超过80%持续1分钟就告警拒绝策略触发立即告警活跃线程长时间接近maximumPoolSize也告警。5.4 动态调整线程池参数不用重启的服务才是好服务线程池参数不是一成不变的业务流量可能有明显的波峰和波谷。Java的ThreadPoolExecutor原生支持通过 setter 方法动态调整核心参数setCorePoolSize(int)可以调大或调小调小时如果当前线程数大于新值多出来的线程会在空闲后回收。setMaximumPoolSize(int)可以在运行时调整最大线程数。setKeepAliveTime(long, TimeUnit)可以动态调整非核心线程的空闲存活时间。实现动态参数调整的常见做法是将线程池参数放到配置中心如Apollo、Nacos通过监听配置变更来调用对应的setter方法。这样遇到流量高峰时运维同学改一个配置服务在几十秒内就能完成线程池扩容不用重启JVM。这里有一个需要注意的坑setMaximumPoolSize只能在当前池大小和构造时设置的最大值之间调整吗不是。JDK的实现里setMaximumPoolSize会直接更新maximumPoolSize字段但如果你传入的新值小于corePoolSize它会强制将corePoolSize一起调小。所以在动态调整时要保证corePoolSize maximumPoolSize否则会出现参数互相覆盖的诡异情况。6. 线程池在业务代码中的正确姿势6.1 线程池该放在哪里静态持有 vs 每次新建这是我在代码评审中经常提到的一个问题。很多初学者喜欢在方法内部直接new ThreadPoolExecutor(...)这样带来的问题非常明显每次调用都创建新线程池线程无法复用高频调用下线程数会爆炸最终必然OOM。正确的姿势是一个业务逻辑对应一个独立的线程池实例且线程池作为Spring Bean或静态成员持有。不要全局共用一个线程池处理所有类型的任务因为不同任务的优先级、耗时、吞吐模型差异很大混在一起的时候慢任务会把快任务都拖垮。我实际架构中通常这样做一个线程池负责异步短信/消息通知规则是快进快出、不阻塞、可丢弃。一个线程池负责数据同步/报表生成规则是慢、批量、可排队、不能丢。一个线程池负责外部接口并行调用规则是IO密集、数量大、必须限制排队量。6.2 父子任务共用线程池的陷阱还有一个经典坑我当初排查了很久才定位一个任务在线程池A中执行执行过程中又向线程池A提交了子任务然后调用future.get()等待子任务结果。当线程池A的所有线程都执行了这种父任务时它们全部阻塞在future.get()上等待队列中的子任务执行但队列中的子任务需要线程池中的线程来执行——可是线程池已经空了。这就是线程池死锁。解决方案有三种父任务中不阻塞等待子任务而是用回调或异步编排CompletableFuture串起来。父任务和子任务使用不同的线程池。队列设置足够大、线程数设置足够多保证子任务有线程可用但这只是缓解不根治。这个问题的本质是线程池处理的任务模型假设每个任务都能独立完成如果你的任务内部又依赖同线程池的其他任务就破坏了这种模型。6.3 线程池优雅关闭从shutdown()到awaitTermination()还有一个小细节很多人处理Spring容器关闭时线程池直接丢弃不管结果导致正在处理中的任务被中断、数据不一致。正确的关闭流程是executor.shutdown()停止接收新任务已提交任务继续执行。executor.awaitTermination(30, TimeUnit.SECONDS)等待已提交任务执行完成最多等30秒。如果超时则executor.shutdownNow()强制中断记录被中断的任务信息方便补偿。我在Spring Boot项目里一般是写一个PreDestroy方法来管理线程池的关闭PreDestroy public void destroy() { executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { executor.shutdownNow(); log.warn(Executor did not terminate in 30 seconds, force shutdown); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } }这个环节看起来不起眼但在追求数据一致性的业务里线程池优雅关闭是不可省略的安全保障。7. 面试八股文之外的线程池细节一些反直觉的行为7.1 当队列无界时maximumPoolSize的值毫无意义我把这个反直觉的行为放在最后是因为它最能检验你是否真的理解线程池。看这个场景new ThreadPoolExecutor(10, 100, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue())maximumPoolSize是100队列是无界的。你猜线上运行时线程数最多会到多少答案是不会超过10。因为线程数增长到corePoolSize后新任务就进队列排队队列永远不会满所以扩容条件永远不满足。这个时候maximumPoolSize100只是个摆设你实际拥有的线程数只取决于corePoolSize和队列的消费速度。7.2 核心线程的超时回收一个容易被忽略的配置还有一个细节keepAliveTime默认只对超过corePoolSize的线程生效。如果你希望核心线程在空闲时也在超时后被回收需要调用allowCoreThreadTimeOut(true)。这在一些任务量波动极大的场景比如每天只有夜间跑批任务的服务比较有用白天几乎空闲核心线程白白占用内存夜间突发大量任务线程池又可以从0开始快速创建线程。但要注意allowCoreThreadTimeOut(true)需要保证核心线程超时后池里至少还有一个线程存活JDK内部会保证至少保留一个核心线程。7.3 从FutureTask到OutOfMemoryError任务对象引用链回到文章开头那场事故。为什么内存会被打爆因为每个被submit()提交的任务都被包装成FutureTaskFutureTask内部持有callable的引用而callable又持有业务请求的上下文、数据库连接、HTTP Client等对象。队列里积压了上万条这样的任务哪怕单条任务对象只占几百KB积少成多也是几GB的堆内存占用OOM只是时间问题。这也是为什么我在事故复盘后坚持长时间排队对内存不友好。有界队列让任务快速失败总比无界堆积造成OOM要可控得多。拒绝后可以通过补偿机制再处理但JVM宕机的恢复成本要高得多。8. 给新手的线程池实践清单照做就能减少踩坑如果你刚开始在线程池上踩坑我整理了一份可以直接照着做的清单核心线程数不要拍脑袋。先用CPU核数或CPU核数×2起步再用压测校准记住IO密集型的数值通常远大于CPU密集型。队列必须有界。容量初始值建议允许排队秒数 × 峰值QPS压测后再调整。拒绝策略必须可见。不要用默认的AbortPolicy然后不catch异常也不要静默丢弃任务要用自定义策略记录告警。给线程起好名字。自定义ThreadFactory是企业级应用的基本素养。加监控。队列占用、活跃线程数、拒绝次数、任务耗时至少这四个指标要上报。动态化参数。把线程池参数放到配置中心优雅应对流量波动。异常处理要内建。每个任务内部要有自己的异常兜底措施不能依赖线程池隐式吞掉异常。我在实际项目中发现照着这个清单做下来线程池相关的事故可以减少九成以上。剩下的那一成是你代码本身的逻辑问题那是另一套调试技巧了。9. 踩过坑之后的一些题外话我早期也背过不少关于线程池的面试八股文什么七个参数四种拒绝策略执行流程都背得滚瓜烂熟。但真正让我长本事的还是那次线上事故和后来做的无数次压测。技术这东西纸上得来终觉浅。线程池不是背熟了参数就能不出问题的它是要在真实的流量模型下才能验证设计是否合理的组件。如果你也在做线程池相关的方案设计建议不要照搬别人的参数。每个服务的核心逻辑、上下游依赖、流量模型都不一样照搬来的配置很可能是隔靴搔痒。拿着本文讲的这些原理和方法压自己的测、调自己的参、看自己的监控比什么都管用。以后线上再出现类似问题你至少能在十分钟内定位到是线程池行为异常而不是像曾经的我一样在被运维和数据同事连环夺命call时才意识到参数的问题。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →