线程池使用总结

2 篇文章 0 订阅
2 篇文章 0 订阅


近半年,工作中遇到两个使用线程池场景,记录下自己使用线程池心得,先对两个场景进行介绍,然后分析配置线程池参数合理性。
核心参数代表含义不再逐一赘述,针对两个场景说下自己当时配置不同线程池参数依据和想法。

场景1:从阿里云批量下载照片

定时任务,每隔5分钟根据url从阿里云下载已经上传的照片,然后在本地完成压缩打包,再上传到阿里云,每张照片下载任务执行互不影响,采用多线程并行下载可缩短下载时间。

public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = 
            new ThreadPoolExecutor(5, 
                    			   10, 
                    			   1,
                   				   TimeUnit.SECONDS,
                    				new ArrayBlockingQueue<>(1000), 
                    				new ThreadPoolExecutor.AbortPolicy());

部署机器配置: 2核、8G、4个实例,过去一周CPU利用率2.832%

场景2:更新微信公众号下十几万关注用户标签

定时任务,每隔一段时间,用户的数据会发生变化,需要全量计算更新每位用户标签,不同用户之间标签计算和更新可独立进行,采用多线程可缩短定时任务执行时间,提高用户标签时效性。

public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = 
            new ThreadPoolExecutor(5, 
                    			   10, 
                    			   30,
                   				   TimeUnit.SECONDS,
                    				new ArrayBlockingQueue<>(200), 
                    				new ThreadPoolExecutor.CallerRunsPolicy());

部署机器配置: 4核、12G、8实例,花费时间3个小时,CPU平均利用率:8.936%,内存使用率51.14%。

Q1:不同场景下线程数如何选择?

两个场景核心线程数配置相同,当时自己随便填写,同事review代码也没说什么,上线之后任务执行效率也还可以,也没出现CPU过高的场景。查阅综合网上博文,线程数按任务是I/O密集型和CPU密集型分别有对应计算公式。I/O密集型任务主要特性是大量磁盘读写、网络传输;CPU密集型任务特点是CPU使用率超过80%,或者开始CPU使用率10%,数据量增加一点,CPU极速飙高。在查阅的博客中,大量引用了《Java并发编程实践》中线程数计算公式。
I/O密集型任务线程数计算公式:

NThread = Ncpu * 2

CPU密集型任务线程数计算公式:

NThreads = Ncpu * Ucpu * (1 + W/C)

其中,Ncpu = 服务器CPU核数,Ucpu= CPU利用率,W=完成任务等待耗时,C是完成任务CPU计算耗时。都假设CPU利用率100%,当W很小时,NThreads = Ncpu,要进行大量CPU计算任务,一个线程执行一个任务,不用频繁进行线程切换,当W/C = 50%,表示只有一半时间需要使用CPU进行计算,那么可以多创建几个线程,同时执行任务,CPU核心线程在不同线程之间进行切换,保证任务在等待的时候,能够让出CPU资源,充分利用CPU。


对于I/O密集型任务:CPU利用率Ucpu = 100%,W/C >> 1,但考虑到服务器内存有限(创建线程需要消耗大量内存),对于W的取值,一种方式,通过观察任务运行时服务器内存和CPU使用情况,调整找到合适的线程数。另外一种方式,在不想测试情况下,W/C取1,一般就OK。

对于CPU密集型任务: CPU利用率Ucpu = 100%,等待时间趋向于0,公式化简为:

NThreads = Ncpu

根据经验值,一般此时线程数会+1:

NThreads = Ncpu+1

为什么+1,《Java并发编程实践》这么说:

计算密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

NThreads为可以设置线程数上限,corePoolSize <= maximumPoolSize <= NThreads。

场景1从阿里云下载图片到本地,网络传输时间很大,属于I/O密集型,应用所部署服务器Ncpu = 2,套入公式NThreads = 2 * 2 = 4 ,回过头来看,corePoolSize设置成5还算合理,maximumPoolSize = 10,CPU也没有持续飙高现象,在满足需求情况下也还算合适。
需要注意,当corePoolSize != maximumPoolSize时,是希望任务特别多情况下,可以触发创建新线程,那么阻塞队列就不要设置太大,corePoolSize+任务队列容量 < 任务数,场景1中,每次执行的定时任务数量已经在业务上控制最多不超过1000个,而corePoolSize(5)+任务队列容量 (1000) =1005 > 任务数1000就显得不合适了,永远不会触发创建新的线程,失去了设置maximumPoolSize的意义。

  • 场景2 上线之后进行分析

这种方式设置线程数也存在问题,以上计线程数,暗含场景是一个服务器上只配置一个线程池。但实际开发中,一个服务器上经常同时存在多个线程池同时运行,这种情况下通过上述公式计算设置的线程数不一定合适,美团通过设置CPU利用率、阻塞队列中元素满载率和单位时间内Reject触发次数阈值,触发报警,人为干预分析,动态调整线程池参数。

Q2: 存活时间选择

如果相邻任务批次执行间隔较小,可以将存活时间设置的较大些,减少频繁创建和销毁线程时间浪费,提高响应效率。
如果相邻任务批次执行间隔较大,可以将存活时间设置较小,保证线程池在等待下一个批次任务到达时间间隔内,能够释放非核心线程数,释放内存占用。

Q3:阻塞队列的选择

根据线程池执行顺序,只有阻塞队列满了之后,才会触发创建新的线程,所以当corePoolSize != maximumPoolSize,阻塞队列千万不要使用无界队列,否则有可能导致触发OOM,并且maximumPoolSize完全失去设置意义。场景1、2阻塞队列类型和大小选择依据了该原则。

Q4: 拒绝策略选择

当任务实时性和可靠性都要求高情况下,可以选择AbortPolicy,当线程池触发策略时,抛出异常,可以设置报警,提醒运维人员及时对线程池进行优化。
当任务可靠性要求高,但实时性要求不高时,可以采用CallerRunsPolicy,无法处理的任务交给父线程执行,放缓任务加入线程池速度。
场景1可靠性要求高,不允许照片下载失败,否则影响业务功能正常执行,定时任务执行间隔为5分钟,实时性要求不那么高,拒绝策略也可以换成CallerRunsPolicy。场景2也不允许任务丢失,定时任务job时间间隔是1天,采用CallerRunsPolicy还算合适。两个任务都是I/O密集型任务,CPU利用率都不足10%,综合CPU和内存使用率,线程数还可以适量增加。

Conclusions

线程池调参抽象数学模型属于求解多元回归模型参数最优解问题,尤其当一个服务器上同时运行多个线程池时,线程池参数需要动态调整。为了更充分利用服务器CPU资源,都希望将线程数设置的尽可能多。采用动态监控线程池参数,设定阈值,超过阈值自动报警,分析调节线程池参数方式,优点,可以较更好利用CPU资源,提高任务执行效率,缺点,需要不断分析调整,人力占用大。设置一个较低的线程数方式,优点,对线程池改动频率低,节省人力,缺点,服务器CPU资源无法充分利用起来,任务执行效率一般。老话重提,根据公司业务规模和需求选择不同设置线程池参数方式。
虽然,乍一看,动态调整线程池确定合适参数是根据经实验不断尝试得出的结果,但掌握线程池参数分析可以指明调参方向,仍有必要掌握。

----- 2022年09月18日(更新1)

场景1(补充):从阿里云批量下载照片

完整背景: 第一步,从阿里云将全部照片下载到本地服务器,第二步、下载到照片的照片压缩,第三步、将压缩包上传到阿里云。
存在问题分析: 第二步和第三步的执行必须在第一步完成之后进行,由于原来设置阻塞队列容量足够大,拒绝策略也没有使用callerRunPolicy(),在照片从阿里云全部下载完成之前,主线程一直处于空闲等待阻塞状态。
线程池参数定义

public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR = 
            new ThreadPoolExecutor(5, 
                    			   10, 
                    			   1,
                   				   TimeUnit.SECONDS,
                    				new SynchronousQueue<>(), 
                    				new ThreadPoolExecutor.CallerRunsPolicy());

相比于之前设计的线程池参数,本版参数设置的好处在于,当核心线程数满的时候,能够快速触发创建新的线程,线程数达到最大线程数时,能够快速触发拒绝策略,利用起来主线程。

Reference:

[1]Java线程池实现原理及其在美团业务中的实践
[2]合理地设置线程池的核心线程数
[3] CPU 密集型 和 IO密集型 的区别,如何确定线程池大小?
[4] 面试官:高并发下,你怎么选择最优的线程数?
[5] 全局线程池和局部线程池
[6] 线程池中线程数量的确定

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值