maximumPoolSize表示线程池允许创建的最大线程数。它不是一开始就生效的。线程池只有在下面几个条件都满足时才会继续创建非核心线程核心线程已经满了队列也满了当前线程数还小于maximumPoolSize这里有一个很容易被忽略的点如果使用的是无界队列那么maximumPoolSize基本就没什么机会生效。因为核心线程满了之后任务会一直进入队列而队列又几乎不会满所以线程数最多也就到corePoolSize。这也是为什么我们不建议用无界队列。无界队列不仅可能导致 OOM还会让最大线程数这个参数失去意义。keepAliveTimekeepAliveTime控制的是非核心线程的空闲存活时间。当线程池里的线程数超过corePoolSize后多出来的线程就是非核心线程。如果这些线程空闲时间超过了keepAliveTime就会被回收。默认情况下核心线程不会因为空闲而回收。如果希望核心线程也能超时回收可以这样设置javathreadPoolExecutor.allowCoreThreadTimeOut(true);不过这个配置要看场景。如果某个线程池使用频率很高核心线程频繁创建和销毁反而会增加开销。如果是低频任务或者任务波动比较大可以考虑让核心线程也支持超时回收。workQueue队列是线程池里非常关键的一个参数。它决定了任务来了以后是先排队还是扩容线程还是直接触发拒绝策略。生产环境里最重要的一点是队列最好有容量限制。ArrayBlockingQueueArrayBlockingQueue是基于数组实现的有界队列创建时必须指定容量javanew ArrayBlockingQueue(1000)它的特点是容量固定内存相对可控比较适合对稳定性要求比较高的业务线程池。缺点是生产者和消费者共用一把锁在并发非常高时吞吐一般但很多业务场景下已经够用了。LinkedBlockingQueueLinkedBlockingQueue是基于链表实现的阻塞队列。它有两种写法javanew LinkedBlockingQueue() new LinkedBlockingQueue(1000)第一种不指定容量就是前面说的高风险写法因为默认容量是Integer.MAX_VALUE。第二种指定容量后是可以使用的。所以问题不在LinkedBlockingQueue这个类本身而在于很多人用了默认构造方法导致队列变成了无界队列。SynchronousQueueSynchronousQueue不存储任务它更像是任务的直接交接。提交任务时如果有空闲线程接收就交给线程执行如果没有空闲线程就看线程池是否还能创建新线程如果不能创建就触发拒绝策略。它适合任务执行时间较短、希望任务不要在队列里堆积的场景。但使用它时一定要控制好maximumPoolSize否则就可能变成线程数暴涨。ThreadFactory线程工厂经常被忽略但线上排查问题时它很重要。比如我们可以给线程设置业务名称javapublic class NamedThreadFactory implements ThreadFactory { private final AtomicInteger count new AtomicInteger(); private final String name; public NamedThreadFactory(String name) { this.name name; } Override public Thread newThread(Runnable r) { Thread thread new Thread(r); thread.setName(name - count.getAndIncrement()); thread.setDaemon(false); return thread; } }这样当线上出现 CPU 飙高、线程阻塞、死锁等问题时通过线程名就能知道是哪个业务线程池出了问题。如果线程名都是默认的pool-1-thread-1排查起来就很难受。RejectedExecutionHandler当线程池达到最大线程数并且队列也满了再提交任务就会触发拒绝策略。JDK 默认提供了几种策略策略说明AbortPolicy直接抛出RejectedExecutionExceptionDiscardPolicy直接丢弃任务不抛异常DiscardOldestPolicy丢弃队列中最早的任务然后重新提交当前任务CallerRunsPolicy由提交任务的线程自己执行任务这几个策略没有绝对好坏要看业务能不能接受任务丢失、能不能接受调用方被阻塞。如果任务不能丢通常不能直接用DiscardPolicy。如果希望问题尽快暴露可以使用AbortPolicy但调用方要处理好异常。如果使用CallerRunsPolicy任务不会被丢但提交任务的线程会被拖住。比如一个 HTTP 请求线程提交异步任务结果线程池满了这个异步任务就由请求线程自己执行。如果任务很慢就会拖慢主链路严重时还可能把 Tomcat 线程池也拖住。所以拒绝策略最少要做两件事记录日志打监控或报警任务被拒绝说明线程池已经饱和了这不是普通异常而是系统处理能力不足的信号。线程池参数怎么定线程池参数没有一个通用答案。比如同样是 8 核机器一个线程池是做本地计算另一个线程池是调用下游接口这两个线程池的参数就不应该一样。通常可以先按下面这个思路来定初始值先区分任务类型是 CPU 密集型还是 IO 密集型估算单个任务耗时以及任务中等待 IO 的比例根据机器资源给一个初始线程数队列一定要有容量限制拒绝策略要结合业务语义选择上线后通过监控观察再调整参数比如一个调用外部接口的异步任务耗时主要在网络等待上可以适当把线程数调大一些但如果任务里有大量计算就不能盲目加线程因为线程太多反而会让 CPU 花更多时间做上下文切换。另外还要注意一点队列容量不是越大越好。队列大只是能放更多任务不代表处理能力变强。如果队列一直在涨本质上说明消费能力已经跟不上了。队列越大任务等待时间可能越长用户感知到的延迟也可能越明显。所以线程池要看的不是“能不能放得下”而是“能不能及时处理完”。项目里一般怎么封装线程池参数理解清楚以后落到项目里还要解决另一个问题不能让每个业务方都按自己的习惯创建线程池。否则线程名、队列容量、拒绝策略和监控方式都会变得不统一。比较稳妥的做法是提供统一入口让业务只关心线程池名称和必要参数底层统一补齐有界队列、命名线程工厂、拒绝策略、监控采集和动态配置。在项目中最好不要让业务代码到处自己 newThreadPoolExecutor。因为每个人写法不一样有的人不设置线程名有的人用无界队列有的人没有拒绝策略有的人没有监控。最后项目里线程池越来越多出了问题也不好查。比较常见的做法是封装一个统一的工具类或者组件业务方通过统一入口创建线程池。比如我们可以提供一个方法javaDynamicExecutorHelper.getExecutor(name, size, queueSize)这里至少要做到几件事线程池名字由业务传入队列容量必须显式传入线程工厂统一设置线程名拒绝策略统一打日志和监控定时采集线程池指标支持从配置中心动态调整线程数包装任务传递 MDC 或 trace 上下文下面看一个简化后的实现思路。统一创建线程池核心创建逻辑可以写成这样javapublic static ExecutorService getExecutor(String name, int size, int queueSize) { ExecutorWrapper executorWrapper executorWrapperCache.getIfPresent(name); if (executorWrapper null) { synchronized (DynamicExecutorHelper.class) { executorWrapper executorWrapperCache.getIfPresent(name); if (executorWrapper null) { ensureMonitorInitialized(); ThreadPoolExecutor threadPoolExecutor new ThreadPoolExecutor( size, size, 1, TimeUnit.MINUTES, queueSize 0 ? new SynchronousQueue() : new LinkedBlockingDeque(queueSize), new NamedThreadFactory(name), new ExecutorRejectedExecutionHandler(name) ); executorWrapper new ExecutorWrapper(name, threadPoolExecutor); executorWrapperCache.put(name, executorWrapper); rejectCounters.put(name, new AtomicInteger(0)); } } } return executorWrapper.getWrapperExecutorService(); }这里有几个点比较关键。第一线程池按名称缓存同一个业务线程池不会重复创建。第二队列没有使用默认无界队列javaqueueSize 0 ? new SynchronousQueue() : new LinkedBlockingDeque(queueSize)如果queueSize 0就使用有界队列如果queueSize 0就使用SynchronousQueue表示任务不排队。第三线程工厂和拒绝策略都是统一的这样线程名、日志、监控都能统一起来。线程池要有监控线程池创建出来以后不能只管提交任务还要定时采集指标。比如javaprivate static void recordMetrics(String name, ThreadPoolExecutor threadPoolExecutor) { SMonitor.recordOne(dynamic_executor_core_ name _ threadPoolExecutor.getCorePoolSize()); SMonitor.recordOne(dynamic_executor_max_ name _ threadPoolExecutor.getMaximumPoolSize()); SMonitor.recordOne(dynamic_executor_active_ name _ threadPoolExecutor.getActiveCount()); SMonitor.recordOne(dynamic_executor_pool_size_ name _ threadPoolExecutor.getPoolSize()); SMonitor.recordOne(dynamic_executor_queue_size_ name _ threadPoolExecutor.getQueue().size()); SMonitor.recordOne(dynamic_executor_queue_remain_cap_ name _ threadPoolExecutor.getQueue().remainingCapacity()); SMonitor.recordOne(dynamic_executor_completed_task_ name _ threadPoolExecutor.getCompletedTaskCount()); }这些指标里最常看的有两个javathreadPoolExecutor.getActiveCount(); threadPoolExecutor.getQueue().size();getActiveCount()能看到当前有多少线程正在执行任务。getQueue().size()能看到有多少任务正在排队。如果活跃线程数长期接近线程池大小说明线程基本都在忙。如果队列长度持续上涨说明任务已经开始堆积。这两个指标要结合起来看。只有活跃线程高不一定有问题可能只是高峰期但如果活跃线程高同时队列也一直涨那就要关注了。拒绝任务要能看到拒绝策略里不要静默处理。可以类似这样javapublic static class ExecutorRejectedExecutionHandler implements RejectedExecutionHandler { private final String name; public ExecutorRejectedExecutionHandler(String name) { this.name name; } Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (!executor.isShutdown()) { SMonitor.recordOne(dynamic_executor_task_reject_ name);