如何正确设置线程池的线程数,提高性能

往往在生产环境中,工作线程数,设置小了,无法充分利用CPU资源,性能会下降。设置大,线程上下文切换过于频繁(可以关注一下协程,如java库提供的Quasar fiber轻量级线程、kilim以及kotlin语言支持的协程。java线程是用户线程与内核线程之间映射的,1:1模式,是通过内核完成调度的。而协程是N个内核线程多路复用M个协程的,N:M的模式,也就是在用户态的协程调度器完成的,减少了与内核之前的交互。可以很好的避免上下文切换。),反而会使性能降低.

如何设置线程池的线程数

首先我们先理解多线程执行的类型分为CPU密集型和IO密集型两种。
CPU密集型,程序计算相关的(如定价里的规则引擎计算逻辑可以视为CPU密集型),消耗CPU资源,线程数可以小点。
IO密集型,网络、磁盘IO操作,如访问数据库,RPC调用,缓存之类的,这部分都会存在等待时间,不占用CPU,我们可以设置大点,充分利用CPU资源。

线程池使用,最好使用自定义的线程池,这也是阿里规范强调的,代码是从源码粘贴过来的。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

默认情况下,线程池中并没有任何线程,等到有任务来才创建线程去执行任务。或者可以调用 prestartAllCoreThreads() 或者 prestartCoreThread(),提前创建核心线程数(我们的业务逻辑应该不适用这个,所以可以忽略)。线程池内部执行原理,我用一个流程表示(临时用ProcessOn画的,然后截图):
在这里插入图片描述

下面根据业务场景模拟一下怎么去设计更合理的线程数,这是我个人根据公式总结并理解出来的。
在这里插入图片描述

1)从工作队列里拿出任务,本地会做一下参数封装
2)访问cache拿一些数据
3)拿到cache里的数据后,再进行一些业务逻辑相关
4)通过RPC调用下游service再拿一些数据
5)RPC调用结束后,再进行业务逻辑相关
6)访问DB进行一些数据操作
7)操作完数据库之后做一些收尾工作,在进行业务逻辑相关。
分析整个流程图:
1)其中1,3,5,7步骤中,线程进行本地业务逻辑计算时需要占用CPU资源,假设计算执行时间是100ms
2)而2,4,6步骤中,访问cache、service、DB过程中线程处于一个等待结果的状态,不需要占用CPU,假设等待时间也是100ms
得到的结论:假设此时是单核,则设置为2个工作线程就可以把CPU充分利用起来,让CPU跑到100% ;假设此时是N核,则设置为2N个工作现场就可以把CPU充分利用起来,让CPU跑到N*100%。

如果业务不是很复杂,CPU密集型可以设置为N+1,IO密集型设置2N的线程数(Netty源码也是默认用的这个公式)。如果业务涉及很多业务逻辑,可以使用:线程数=N(CPU核数)*(1+(线程等待时间)/(线程时间运行时间))

但是在设置合理的线程数之前,我们还得需要考虑一下DB的连接数,以及其他RPC服务的承载能力,别自己服务性能提升了几倍,影响到其他服务性能。比如云库默认连接池是2000,我们总共60机器,单台机器设置的线程数是40,那么打到DB上的请求是40X60=2400,那么DB拒绝连接了。

还有一个注意点:举例clover,它本身就是存在线程池的,然后我们业务逻辑又初始化线程池达到异步效果(线程池共享的)。如clover设置了10个,但是内部线程池设置了10或者是小于10,这样往往达不到你预期的效果,你可以把内部的线程池调大点,或者是去掉内部线程池。这个之前compleableFuture也强调了这点。

案例代码:

下面我本地测试代码(8核的),比较了IO密集型和CPU密集型,根据线程数不断的增加,线程池执行整体执行时间会越来越快,但是每个线程平均执行的越来越大。然后发现IO的在16个线程比较合理。CPU可以8-16之间比较合理。测试数据比较(只是参考数据,还得考虑你们的测试环境,数据可能有所不同):

工作队列正确设置

如果线程处理业务非常快,我们可以考虑将阻塞队列设置大一些,处理的请求吞吐量会大些;如果线程处理业务非常耗时,阻塞队列设置小些,防止请求在阻塞队列中等待过长时间而导致请求已超时。最好使用有界队列,以防因队列过大而导致的内存溢出问题

队列建议:阻塞队列ArrayBlockingQueue(有界队列)、LinkedBlockingQueue(需要设置队列大小,使用读写锁分离,性能上会比Array好点,但是要注意初始化大小,否则是无界队列),非阻塞队列ConcurrentLinkedQueue(无界队列,并发性能会比阻塞队列要好,使用了CAS无锁机制,但需要关注无界的)。里面的实现可以看一下源码,可以避免使用不当,就不贴源码了。

还有一个注意点,在使用Spring自带的线程池ThreadPoolTaskExecutor,在初始化队列的地方(如源码),这里我们完全可以重写createQueue方法,实现我们想要的队列。

protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
		if (queueCapacity > 0) {
			return new LinkedBlockingQueue<>(queueCapacity);
		}
		else {
			return new SynchronousQueue<>();
		}
	}

上面只是列举了线程数和队列的使用,线程池还有其他的参数,可以根据自己的业务去配置,比如拒绝策略怎么选择,设置符合自己的线程名称,以及空闲线程的时间。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值