当前位置: 首页 > news >正文

maximumPoolSize

maximumPoolSize表示线程池允许创建的最大线程数。

它不是一开始就生效的。线程池只有在下面几个条件都满足时,才会继续创建非核心线程:

  1. 核心线程已经满了
  2. 队列也满了
  3. 当前线程数还小于maximumPoolSize

这里有一个很容易被忽略的点:如果使用的是无界队列,那么maximumPoolSize基本就没什么机会生效。

因为核心线程满了之后,任务会一直进入队列,而队列又几乎不会满,所以线程数最多也就到corePoolSize

这也是为什么我们不建议用无界队列。无界队列不仅可能导致 OOM,还会让最大线程数这个参数失去意义。

keepAliveTime

keepAliveTime控制的是非核心线程的空闲存活时间。

当线程池里的线程数超过corePoolSize后,多出来的线程就是非核心线程。如果这些线程空闲时间超过了keepAliveTime,就会被回收。

默认情况下,核心线程不会因为空闲而回收。

如果希望核心线程也能超时回收,可以这样设置:

java

threadPoolExecutor.allowCoreThreadTimeOut(true);

不过这个配置要看场景。

如果某个线程池使用频率很高,核心线程频繁创建和销毁反而会增加开销。如果是低频任务,或者任务波动比较大,可以考虑让核心线程也支持超时回收。

workQueue

队列是线程池里非常关键的一个参数。

它决定了任务来了以后,是先排队,还是扩容线程,还是直接触发拒绝策略。

生产环境里,最重要的一点是:队列最好有容量限制。

ArrayBlockingQueue

ArrayBlockingQueue是基于数组实现的有界队列,创建时必须指定容量:

java

new ArrayBlockingQueue<>(1000)

它的特点是容量固定,内存相对可控,比较适合对稳定性要求比较高的业务线程池。

缺点是生产者和消费者共用一把锁,在并发非常高时吞吐一般,但很多业务场景下已经够用了。

LinkedBlockingQueue

LinkedBlockingQueue是基于链表实现的阻塞队列。

它有两种写法:

java

new LinkedBlockingQueue<>() new LinkedBlockingQueue<>(1000)

第一种不指定容量,就是前面说的高风险写法,因为默认容量是Integer.MAX_VALUE

第二种指定容量后,是可以使用的。

所以问题不在LinkedBlockingQueue这个类本身,而在于很多人用了默认构造方法,导致队列变成了无界队列。

SynchronousQueue

SynchronousQueue不存储任务,它更像是任务的直接交接。

提交任务时,如果有空闲线程接收,就交给线程执行;如果没有空闲线程,就看线程池是否还能创建新线程;如果不能创建,就触发拒绝策略。

它适合任务执行时间较短、希望任务不要在队列里堆积的场景。

但使用它时一定要控制好maximumPoolSize,否则就可能变成线程数暴涨。

ThreadFactory

线程工厂经常被忽略,但线上排查问题时它很重要。

比如我们可以给线程设置业务名称:

java

public 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直接抛出RejectedExecutionException
DiscardPolicy直接丢弃任务,不抛异常
DiscardOldestPolicy丢弃队列中最早的任务,然后重新提交当前任务
CallerRunsPolicy由提交任务的线程自己执行任务

这几个策略没有绝对好坏,要看业务能不能接受任务丢失、能不能接受调用方被阻塞。

如果任务不能丢,通常不能直接用DiscardPolicy

如果希望问题尽快暴露,可以使用AbortPolicy,但调用方要处理好异常。

如果使用CallerRunsPolicy,任务不会被丢,但提交任务的线程会被拖住。比如一个 HTTP 请求线程提交异步任务,结果线程池满了,这个异步任务就由请求线程自己执行。如果任务很慢,就会拖慢主链路,严重时还可能把 Tomcat 线程池也拖住。

所以拒绝策略最少要做两件事:

  1. 记录日志
  2. 打监控或报警

任务被拒绝说明线程池已经饱和了,这不是普通异常,而是系统处理能力不足的信号。

线程池参数怎么定

线程池参数没有一个通用答案。

比如同样是 8 核机器,一个线程池是做本地计算,另一个线程池是调用下游接口,这两个线程池的参数就不应该一样。

通常可以先按下面这个思路来定初始值:

  1. 先区分任务类型,是 CPU 密集型还是 IO 密集型
  2. 估算单个任务耗时,以及任务中等待 IO 的比例
  3. 根据机器资源给一个初始线程数
  4. 队列一定要有容量限制
  5. 拒绝策略要结合业务语义选择
  6. 上线后通过监控观察,再调整参数

比如一个调用外部接口的异步任务,耗时主要在网络等待上,可以适当把线程数调大一些;但如果任务里有大量计算,就不能盲目加线程,因为线程太多反而会让 CPU 花更多时间做上下文切换。

另外还要注意一点:队列容量不是越大越好。

队列大,只是能放更多任务,不代表处理能力变强。如果队列一直在涨,本质上说明消费能力已经跟不上了。队列越大,任务等待时间可能越长,用户感知到的延迟也可能越明显。

所以线程池要看的不是“能不能放得下”,而是“能不能及时处理完”。

项目里一般怎么封装线程池

参数理解清楚以后,落到项目里还要解决另一个问题:不能让每个业务方都按自己的习惯创建线程池。否则线程名、队列容量、拒绝策略和监控方式都会变得不统一。

比较稳妥的做法是提供统一入口,让业务只关心线程池名称和必要参数,底层统一补齐有界队列、命名线程工厂、拒绝策略、监控采集和动态配置。

在项目中,最好不要让业务代码到处自己 newThreadPoolExecutor

因为每个人写法不一样,有的人不设置线程名,有的人用无界队列,有的人没有拒绝策略,有的人没有监控。最后项目里线程池越来越多,出了问题也不好查。

比较常见的做法是封装一个统一的工具类或者组件,业务方通过统一入口创建线程池。

比如我们可以提供一个方法:

java

DynamicExecutorHelper.getExecutor(name, size, queueSize)

这里至少要做到几件事:

  1. 线程池名字由业务传入
  2. 队列容量必须显式传入
  3. 线程工厂统一设置线程名
  4. 拒绝策略统一打日志和监控
  5. 定时采集线程池指标
  6. 支持从配置中心动态调整线程数
  7. 包装任务,传递 MDC 或 trace 上下文

下面看一个简化后的实现思路。

统一创建线程池

核心创建逻辑可以写成这样:

java

public 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(); }

这里有几个点比较关键。

第一,线程池按名称缓存,同一个业务线程池不会重复创建。

第二,队列没有使用默认无界队列:

java

queueSize <= 0 ? new SynchronousQueue<>() : new LinkedBlockingDeque<>(queueSize)

如果queueSize > 0,就使用有界队列;如果queueSize <= 0,就使用SynchronousQueue,表示任务不排队。

第三,线程工厂和拒绝策略都是统一的,这样线程名、日志、监控都能统一起来。

线程池要有监控

线程池创建出来以后,不能只管提交任务,还要定时采集指标。

比如:

java

private 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()); }

这些指标里,最常看的有两个:

java

threadPoolExecutor.getActiveCount(); threadPoolExecutor.getQueue().size();

getActiveCount()能看到当前有多少线程正在执行任务。

getQueue().size()能看到有多少任务正在排队。

如果活跃线程数长期接近线程池大小,说明线程基本都在忙。

如果队列长度持续上涨,说明任务已经开始堆积。

这两个指标要结合起来看。只有活跃线程高,不一定有问题,可能只是高峰期;但如果活跃线程高,同时队列也一直涨,那就要关注了。

拒绝任务要能看到

拒绝策略里不要静默处理。

可以类似这样:

java

public 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);
http://www.jsqmd.com/news/1153132/

相关文章:

  • 4TB海量空间握在掌心,随身携带的不再是取舍而是全部资料库
  • PvZ Toolkit:植物大战僵尸PC版内存修改器深度解析与实战指南
  • vROps 8.6 兼容哪些 vCenter 版本?6.7/7.x/8.x 适配清单
  • 北京华恒智信为文旅夜游行业搭建专属人力编制与运营排班体系
  • 计算机毕业设计之流行音乐网站
  • 【办公自动化】 OpenClaw 完整搭建方案 规避各类部署报错技巧(含安装包)
  • BEVFormer v2.0 多相机感知实战:4D BEV特征构建与3D目标检测代码解析
  • 科研不是拼积木:沁言学术 vs WPS AI,谁才是科研全流程最优解?
  • AI时代程序员必看:5项核心竞争力助你升级,收藏这份成长指南!
  • vtkEDLShading代码解读
  • 深入理解 Linux 内存、Swap 交换分区与分页机制的关系
  • 出海网络环境怎么搭?9HTTP 真实使用体验分享
  • Agent工具的学习成本高吗?上手快不快?——企业级智能体选型与工程化落地深度横评
  • Windows资源管理器毛玻璃效果终极指南:ExplorerBlurMica让你的桌面焕然一新
  • 2026年AI简历优化实战:从47分到86分,我用STAR-C升维法让JD匹配度飙升
  • Spring-ai 提示词动态模板
  • 灵魂面甲开服进服教程 低延迟可支持模组开服
  • Scikit-learn 1.4 特征工程:3类编码方案与5个特征选择方法实战解析
  • 品牌为什么总在季前埋下库存风险?关键在商品计划和OTB预算
  • lvgl字体转化笔记(基于紧凑格式)
  • C++ 中 RDTSC 读取方式优化:从内联汇编到 __rdtsc Intrinsic
  • MapGIS Cloud Portal软件介绍
  • 南京GEO优化服务商排行榜
  • 药物靶标鉴定为什么选择DARTS技术
  • 10 个 Design Axes,分 3 组:
  • 鸿蒙面试题系列(一):从状态管理到性能优化,这7个核心考点你掌握了没?
  • 项目计划基线管理:Git版本控制思维在计划变更中的迁移实践
  • 2026小白创业必备:零基础线上开店系统推荐及功能详解
  • 如何高效实现双路FOC电机控制:专业级驱动方案深度解析
  • 如何快速解锁加密音乐:免费浏览器工具完整指南