线程池参数调优?别背八股了,面试官想听的是这个
【难度:★★★】【频率:高频】【适用:1-3年】
面试官问:线程池核心参数怎么设?拒绝策略怎么选?
场景是一个电商系统的订单处理服务,核心业务是接收用户下单请求,异步调用库存、优惠券、积分三个下游服务。线上偶尔出现接口超时,怀疑是线程池配置不合理。
面试官问:这个场景下,线程池的核心线程数、最大线程数、队列长度你怎么定?拒绝策略选哪个?
为什么这么问:考察的不是背出七大参数和四种拒绝策略,而是有没有真正处理过线上问题。面试官在评估:能不能根据业务场景算参数,能不能说清楚每个参数的取舍,以及拒绝策略选错会有什么后果。
怎么回答:
先给结论,再给推导过程。
核心线程数,按CPU密集型和IO密集型分开算。订单服务是典型的IO密集型,下游调用全是网络请求,线程大部分时间在等待。公式是:核心线程数 = CPU核数 / (1 - 阻塞系数),阻塞系数在IO密集场景可以取0.8到0.9。假设线上是4核8G,核心线程数可以设10到12。
最大线程数,留出峰值缓冲。核心线程处理不过来,任务进队列,队列满了才开新线程。最大线程数可以设为核心线程数的2倍左右,比如20到24。但要注意,线程多了上下文切换开销也大,不是越多越好。
队列长度,这个最容易拍脑袋。用有界队列,长度设500到1000。选有界队列的原因很直接:无界队列会让最大线程数形同虚设,任务全堆在内存里,OOM了都不知道怎么死的。
拒绝策略,默认的AbortPolicy直接抛异常,对用户不友好。CallerRunsPolicy在调用线程里执行被拒绝的任务,相当于把压力传回给调用方,适合对实时性要求不高的场景。订单场景更推荐这个——下单请求是用户主动触发的,用调用线程执行至少保证不丢,用户等个几百毫秒能接受。
回答时可以把计算过程写出来:
ThreadPoolExecutor orderPool = new ThreadPoolExecutor(
12, // 核心线程数
24, // 最大线程数
60, TimeUnit.SECONDS, // 空闲线程存活时间
new ArrayBlockingQueue<>(800), // 有界队列
new ThreadFactoryBuilder().setNameFormat("order-pool-%d").build(),
new CallerRunsPolicy() // 拒绝策略
);
关键点在两个地方。一是线程工厂必须自定义,给线程起名字,不然出问题查日志都不知道哪个池子里的线程在跑。二是拒绝策略选了CallerRunsPolicy,意味着下单接口自身可能被阻塞,接口的超时时间要留够余量。
怎么拓展:
如果只答到这儿,面试官会觉得你是背过题的。往下挖一层。
队列长度800,这个数字怎么来的?不是拍脑袋。订单服务高峰期QPS大概2000,下游平均响应时间200毫秒,那么每秒产生的任务量是400。如果允许任务在队列里最多等2秒,队列长度就是800。这个推导过程比参数本身值钱。
再深一层,说一个真实踩过的坑。线上曾经把核心线程数设成8,最大线程数设成16,队列长度1000。结果发现CPU使用率只有30%,但接口P99延迟到了2秒。查下来发现是核心线程数太小,任务全在队列里排队,线程根本不够用。把核心线程数提到16之后,延迟直接降了一半。这个案例说明,核心线程数不是拍脑袋定的,要结合流量峰值和下游延迟来算。
可能追问:
面试官大概率会问:CallerRunsPolicy把任务丢回调用线程,那调用线程自己会不会被拖死?
这问的是拒绝策略的边界。可以这么接:会,所以CallerRunsPolicy不是万能的。如果调用线程是Tomcat的工作线程,任务堆积会让Tomcat线程池也满,最后HTTP 503。更稳妥的做法是配合降级——在拒绝策略里做异步落库,把任务写到消息队列,等高峰期过了再慢慢消费。这个方案等于把线程池的背压转嫁给MQ,削峰填谷。
再追问一层:那MQ本身会不会被打爆?这时候就可以说,MQ的消费速度由下游决定,如果下游扛不住,MQ堆积是预期内的,至少不会丢数据。这个回答展示了从线程池到MQ到下游的全局思考,面试官会认为有架构视野。
本文关键词:线程池、拒绝策略、参数调优、CallerRunsPolicy