见字如面,我是威哥,一个从普通二本院校毕业,从未曾接触分布式、微服务、高并发到通过技术分享实现职场蜕变,成长为RocketMQ社区优秀布道师、大厂资深架构师,出版《RocketMQ技术内幕》一书,欢迎大家关注我,一起交流进步。
JAVA并发编程是大厂第一轮面试中的高频面试题,而线程池又是其中的典型代表,本文将梳理关于线程池的工作机制,并提出灵魂之问:你对线程池的工作机制这么了解,那你在工作中是如何判断一个线程池需要创建多少个线程的呢?
JAVA 线程池的核心属性如下:
那当用户向线程池提交一个任务的时候,线程池会如何创建线程呢?
温馨提示:所以如果线程池使用的队列无界队列,最大线程数会变得没有意义。
JUC 默认提供了如下拒绝策略:
拒绝策略触发的条件:线程池使用的是有界任务队列时,才有可能被触发,当队列已满,并且线程池创建的线程已经达到了最大允许的线程池时。
默认情况下,通常使用 AbortPolicy 即可。
CallerRunsPolicy 异步转同步在出现拒绝的情况下其实意义不大,没有想出其合适的场景,因为需要执行拒绝策略的时候,已经处理变慢了,再同步执行任务,只会增加服务器的负载,不利于恢复问题。
DiscardOldestPolicy 这种策略,通常用于类似记录轨迹,偶尔丢失点数据没关系,但希望最新的数据能得到保存。
DiscardPolicy 策略,通常用来异步打印日志,直接忽略不执行,期望保存旧的数据。
阿里内部的开源规范明确禁止使用无界队列,如果使用无界队列,任务会不受限制的往线程池中提交,有可能造成内存溢出。
如果使用无界队列,最大线程数这个参数将会失效,因为永远也不会创建多于核心线程数量的线程。
ThreadFactory threadFactory,线程池工厂,在使用线程池时,强烈推荐使用自己定义的线程工厂,这样能为线程池中的线程进行命名,方便跟大家使用 jsatck 命令查看线程栈时,能快速识别对应的线程。
keepAliveTime :通俗点来说,这个参数表示线程的最大空闲时间,即如果线程没有在执行任务,能存活的时间。
默认情况下,该参数只针对超过核心线程数(corePoolSize) 的线程,可通过将allowCoreThreadTimeOut设置为true,则核心线程数也会因为空闲而被关闭。
目前根据我看过的一些开源框架,设置多少个线程数量通常是根据应用的类型:IO密集型、CPU密集型。
实际情况往往复杂得多,并不会按照这个进行设置,上面的公司通常适合框架设置IO线程的个数,例如netty,dubbo这种底层通讯框架通常会参考上述标准进行设置。
关于在实际业务开发中,如何为一个线程池设置合适的线程呢?
其实对于IO密集型类型的应用,网上还有一个公式:线程数 = CPU核心数/(1-阻塞系数)
引入了阻塞系数的概念,一般为0.8~0.9之间,
在我们的业务开发中,基本上都是IO密集型,因为往往都会去操作数据库,访问redis,es等存储型组件,都会涉及到磁盘IO,网络IO。
那什么场景下是CPU密集型呢?纯计算类,例如计算圆周率的位数,当然我们基本接触不到。
IO密集型,可以考虑多设置一些线程,主要目的是可以增加IO的并发度,CPU密集型不宜设置过多线程,因为是会造成线程切换,反而损耗性能。
接下来我们以一个实际的场景来说明如何设置线程数量。
一个4C8G的机器上部署了一个MQ消费者,在RocketMQ的实现中,消费端也是用一个线程池来消费线程的,那这个线程数要怎么设置呢?
如果按照 2n + 1 的公式,线程数设置为 9个,但在我们实践过程中发现如果增大线程数量,会显著提高消息的处理能力,说明 2n + 1 对于业务场景来说,并不太合适。
如果套用 线程数 = CPU核心数/(1-阻塞系数) 阻塞系数取 0.8 ,线程数为 20 。阻塞系数取 0.9,大概线程数40,20个线程数我觉得可以。
如果我们发现数据库的操作耗时比较多,此时可以继续提高阻塞系数,从而增大线程数量。
那我们怎么判断需要增加更多线程呢?可以用jstack命令查看一下进程的线程栈,如果发现线程池中大部分线程都处于等待获取任务,则说明线程够用,如下图所示:
如果大部分线程都处于运行状态,可以继续适当调高线程数量。
本期就介绍到这里了,希望对你有所帮助,同时也希望一键三连,给作者一些鼓励。
分享笔者关于RocketMQ线上故障案例剖析的电子书,私信回复RMQPDF即可获取。