ThreadPoolExecutor参数该如何设置——java并发系列(三)

ThreadPoolExecutor参数设置

回顾一下ThreadPoolExecutor的参数

ThreadPoolExecutor有很多构造方法以及构造方式,最后都会调用一个七个参数的方法,参数的含义分别是:

  • int corePoolSize: 核心线程数
  • int maximumPoolSize: 最大线程数
  • long keepAliveTime & TimeUnit unit: 最大空闲时间
  • BlockingQueue workQueue: 工作队列,是一个阻塞队列
  • ThreadFactory threadFactory: 线程工厂,可以给线程起一个有意义的名字,也可以对线程进行扩展,比如给线程设置UncaughtExceptionHandler;
  • RejectedExecutionHandler handler: 拒绝策略,当上面的工作队列,是一个有界队列的时候,如果队列已经满了的场景的执行策略

那么这些参数该如何设置呢?

一、corePoolSize & maximumPoolSize

1.1 计算密集型 vs IO密集型

首先,我们需要识别该线程池运行的任务类型

计算密集型

计算密集型:加解密、压缩等耗费CPU资源的任务。

这种任务,CPU的核数为任务的瓶颈,线程数设置过多,只会导致线程的频繁上下文切换,进而导致性能下降。因此线程数量可以设置为CPU核数+1,这个+1,可以保证,即使线程由于偶尔出现缺页等原因导致暂停,也可以由这个“额外”的线程,保证CPU的时钟不会被浪费。

IO密集型

IO密集型:文件读写、网络通信、RPC调用(网络通信的一种)等任务。

这种任务,大部分时间阻塞在IO上,线程只是在等待,并没有真正的被利用起来,因此线程数可以设置的更大,一个经验值为2 * CPU核数。不过这只是一个简单的经验值,理论上可以按照以下公式,计算线程数。

整理了这份Java面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

代码解读

复制代码

线程数 = CPU核心数 * 线程运行总时间/线程计算时间

这个公式的核心思想就是,充分利用CPU,不让CPU空闲。这里线程运行总时间=线程等待时间+线程计算时间,经过转换,可以得到一个网上更常见的公式。

 

scss

代码解读

复制代码

线程数 = CPU核心数 * ( 1 + 线程等待时间/线程计算时间)

有时,我们并不希望CPU被一种类型的任务全部占满,因此可能会有个CPU期望利用率,则有以下公式,这就是《JAVA 并发实战》中的理论线程数量:

 

scss

代码解读

复制代码

线程数 = CPU核心数 * CPU期望利用率 ( 1 + 线程等待时间/线程运行总时间)


🤔CPU核心数如何获取

 

java

代码解读

复制代码

int cupNumbers = Runtime.getRuntime().availableProcessors();

1.2 其他瓶颈

以上指定线程数的策略,主要是考虑了CPU的制约,然而,实际上任务的瓶颈,还收到其他因素的影响。比如内存、数据库连接池等因素。

  • 比如我们数据库连接池的设置为10,那么我们即使线程池设置为100,也没什么意义,即使100个线程并发,也最多有10个线程真正执行,其他线程都在获取数据库连接处阻塞等待。
  • 又或者,一个查询任务,会有大量的数据返回,占用大量内存,这时也需要控制并发数量,防止JVM内存被打爆,而产生OOM异常。

除此之外,我们的JVM上,一般也不会只有一个线程池在运行,如果按照以上的理论值配置,可能把所有的CPU资源都占满。另外还要结合线上的真实QPS,如果QPS很低,而线程数设置很高,也是一种浪费。因此,线程池的数量,是一个考虑多方面因素制约,取一个合理的值。

1.3 压测

真实的运行环境,往往比较复杂,我们可以采用压测的手段,不断调整参数,并结合业务特点,得到一个合理的线程数量。

  • 对于线上的QPS/TPS很高任务,我们可以采用与压测结果比较接近的线程数,比如压测出500线程数效果较好,600也还可以,那么可以把核心&最大线程数,分别设置为500和600。
  • 对于线上QPS较低的业务,可以设置较少的线程数量,如果非常低,一天甚至一个月都没有几次访问,又不是核心业务,甚至可以调用allowCoreThreadTimeOut方法,设置核心线也可以销毁,以免浪费线程资源。

二、keepAliveTime & TimeUnit unit

keepAliveTime & TimeUnit unit是最大空闲时间,如果一个线程在线程池中空闲时间,超过了指定值,则会触发判断,当前线程数量是否超过核心线程数量,超过,则尝试销毁线程资源,避免浪费。

最大线程数这个参数,就是为了抗住线上的峰值流量,如果核心线程处理不过来,并且用于缓冲的阻塞队列已经占满,则启动额外的线程(如果最大线程数大于核心线程数),处理流量。那么,这个空闲时间,需要基于线上的流量判断,找到线上高QPS的周期性。假设线上9点到12点,每半个小时有一次高峰,那么建议空闲时间大于这个周期值,防止非核心线程刚销毁,就迎来了另一波高峰。

三、workQueue

当所有的核心线程都被创建后,新提交的任务,会被提交到这个参数中提供的阻塞队列中。线程池有点类似于生产者、消费者模型,而这个队列就是生产者和消费者之间的缓冲区。

3.1 队列类型

我们先来看看有哪些队列类型,供我们使用:

  • ArrayBlockingQueue : 基于数组结构的有界阻塞队列;
  • LinkedBlockingQueue : 基于链表结构的阻塞队列,可以根据参数控制有界或者无界;
  • SynchronousQueue : 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
  • PriorityBlockingQueue : 具有优先级的无界阻塞队列;
  • DelayedWorkQueue : 延迟队列,可以用于运行定时任务的线程池,比如Executors.newScheduledThreadPool就使用的DelayedWorkQueue。

先对比下我们比较常见的ArrayBlockingQueue和LinkedBlockingQueue:

对比项ArrayBlockingQueueLinkedBlockingQueue
实现基于数组,生产者消费者共用一把ReentrantLock基于链表,生产者消费者分别使用自己的ReentrantLock
队列长度有界,且必须构造时指定长度,无法动态修改可以有界,也可以无界,支持动态改变队列长度
公平性支持不支持
内存初始化时,就需要初始化整个数组使用时才会初始化节点,但存储节点需要额外的引用
性能生产者消费者使用一把锁,锁竞争更加激烈,但数组内存的cpu亲和性更好(更容易被L1/L2缓存)生产者消费者分别使用锁,在锁的角度上竞争没有那么激烈,但链表内存亲和性很差

根据以上对比,我们可以得出什么样的场景选择哪种阻塞队列

  1. 需要无界队列或者动态改变队列长度,选择LinkedBlockingQueue;
  2. 需要一个非常大的队列,建议LinkedBlockingQueue,ArrayBlockingQueue需要预先初始化,占用较大内存;
  3. 需要严格的先进先出,也就是公平性,使用ArrayBlockingQueue;
  4. 较高的QPS下,锁竞争会非常激烈,建议使用LinkedBlockingQueue。

相较于ArrayBlockingQueue和LinkedBlockingQueue,其他的队列有着比较明显的使用场景。

  • SynchronousQueue : 适用于需要较高的吞吐量;
  • PriorityBlockingQueue : 需要实现任务的优先级;
  • DelayedWorkQueue :需要延迟执行,比如定时任务;

3.2 队列长度

不建议使用无界队列,因为如果消费者出现问题(挂了或者延迟很大),会导致阻塞队列不断膨胀,占用大量内存,进而影响进程中的其他任务。

四、ThreadFactory

线程池工厂,这里建议自定义线程池工厂

  1. 可以给线程起一个与线程池运行的任务相关的名字,打印日志的时候,一般会带上线程名字,这样就方便后续问题的排查。
  2. 可以自定义一个UncaughtExceptionHandler,用于处理线程抛出的异常。

五、RejectedExecutionHandler

当线程池阻塞队列打满,并且已经没有额外的非核心线程用于处理这些任务时,线程池根据指定的拒绝策略来执行,官方提供了四种策略

  1. AbortPolicy:默认的拒绝策略,会throws RejectedExecutionException。
  2. CallerRunsPolicy:提交任务的线程自己去执行该任务。
  3. DiscardPolicy:直接丢弃任务,没有任何异常抛出。
  4. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

使用哪种,可以根据业务特点,自行选择。如果都不满足需求,可以自行实现RejectedExecutionHandler。比如:记录日志或者持久化后再抛出异常,用于后续补偿。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值