线程池 ThreadPoolExecutor 的详细讲解

线程池的使用场景

  • 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
  • 并发不高、任务执行时间长的业务要区分开看:
  1. 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
  2. 假如是业务时间长集中在计算操作上,也就是计算密集型任务,线程池中的线程数设置得少一些,减少线程上下文的切换
  • 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考上面。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦

线程池的作用

 

线程使应用能够更加充分合理的协调利用cpu 、内存、网络、i/o等系统资源;线程的创建需要开辟虚拟机栈,本地方法栈、程序计数器等线程私有的内存空间;在线程的销毁时需要回收这些系统资源。频繁的创建和销毁线程会浪费大量的系统资源,增加并发编程的风险。

在服务器负载过大的时候,如何让新的线程等待或者友好的拒绝服务(也就是先不创建线程去执行该操作)?这些丢失线程自身无法解决的。所以需要通过线程池协调多个线程,并实现类似主次线程隔离、定时执行、周期执行等任务。

所以,线程池的作用包括:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,监控和调优
  • 实现任务线程队列缓存策略和拒绝机制

ThreadPoolExecutor 的构造函数

参数介绍:

  • corePoolSize (int)

 

表示常驻核心线程数。是指在没有任务需要执行的时候线程池的大小。如果等于0,则任务执行完成后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁,保持这个线程数。这个值如果设置过大会浪费资源,设置的过小会导致线程频繁地创建和销毁。在刚刚创建ThreadPoolExecutor的时候,线程并不会立即启动,而是要等到有任务提交时才会启动,除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。再考虑到keepAliveTime和allowCoreThreadTimeOut超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是corePoolSize。

 

  • maximumPoolSize (int)

 

线程池中允许的最大线程数,线程池中的当前线程数目不会超过该值。如果队列中任务已满,并且当前线程个数小于maximumPoolSize,那么会创建新的线程来执行任务。这里值得一提的是largestPoolSize,该变量记录了线程池在整个生命周期中曾经出现的最大线程个数。为什么说是曾经呢?因为线程池创建之后,可以调用setMaximumPoolSize()改变运行的最大线程的数目。如果maximumPoolSize 与corePoolSize 相等,即是固定大小线程池

 

  • poolSize(这个不是构造函数的参数,是ThreadPoolExecutor的成员变量)

 

线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize不会超过maximumPoolSize。

  • keepAliveTime (long)

表示线程池中的线程空闲时间,当线程的数量超过corePoolSize ,然后线程的空闲时间达到KeepAliveTime 值时,线程被销毁,直到剩下corePoolSize 个线程为止,避免浪费内存和句柄资源。在默认情况下,当线程池的线程大于corePoolSize 时,keepAliveTime 才会起作用。但是ThreadPoolExecutor的allowCoreThreadTimeOut 变量设置为ture时,核心线程超时后也会被回收(也就是poolSize小于corePoolSize 时,也会根据keepAliveTime进行销毁线程)。

  • TimeUnit (TimeUnit枚举)

表示时间单位。keepAliveTime 的时间单位通常是TimeUnit.SECONDS。

  • workQueue (BlockingQueue<Runnable>)

表示缓存队列。它决定了缓存任务的排队策略。对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的队列。这个队列需要一个实现了BlockingQueue接口的任务等待队列,推荐三种缓存队列,它们是:SynchronousQueue 、LinkedBlockingQueue 和 ArrayBlockingQueue。当前请求的线程数poolSize大于corePoolSize,但是又小于maximumPoolSize时,线程进入BlockingQueue 阻塞队列。

  • threadFactory (ThreadFactory)

表示生产线程的工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个factory增加组名前缀来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。

  • handler (RejectedExecutionHandler)

表示执行拒绝策略的对象。当要创建的线程数量大于线程池的最大线程数,并且缓存队列也满了的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。这是一种简单的限流保护。友好的拒绝策略可以使如下三种:

  1. 保存到数据库进行削峰填谷。在空闲的时候再拿出来执行。
  2. 转向某个提示页面。
  3. 打印日志。

如何来设置ThreadPoolExecutor的参数

需要根据几个值来决定

  1. tasks :每秒的任务数,假设为500~1000
  2. taskcost:每个任务花费时间,假设为0.1s
  3. responsetime:系统允许容忍的最大响应时间,假设为1s

做几个计算

corePoolSize = 每秒需要多少个线程处理? 

threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.1 = 50~100 个线程。corePoolSize设置应该大于50

根据8020原则,如果80%的每秒任务数小于800,那么corePoolSize设置为80即可

queueCapacity = (coreSizePool/taskcost)*responsetime

计算可得 queueCapacity = 80/0.1*1 = 80。意思是队列里的线程可以等待1s,超过了的需要新开线程来执行

切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。

maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)(最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数

计算可得 maxPoolSize = (1000-80)/10 = 92

rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理

keepAliveTime和allowCoreThreadTimeout:采用默认通常能满足

新提交一个任务时的处理流程:

  • 如果线程池的当前大小还没有达到基本大小(poolSize < corePoolSize),那么就新增加一个线程处理新提交的任务;

 

  • 如果当前大小已经达到了基本大小,就将新提交的任务提交到缓存队列排队,等候处理workQueue.offer(command);

 

  • 如果缓存队列容量已达上限,并且当前大小poolSize没有达到maximumPoolSize,那么就新增线程来处理任务;

 

  • 如果缓存队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务,调用拒绝策略。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadPoolExecutorJava 中一个线程池管理器,可以用于管理线程池中的线程。使用 ThreadPoolExecutor 可以有效地控制线程的创建和销毁,从而提高程序的性能和稳定性。 ThreadPoolExecutor 的构造函数有多个参数,常用的参数有以下四个: - corePoolSize:核心线程池大小,即线程池中保持的最小线程数,即使它们处于空闲状态也会保持活动状态。 - maximumPoolSize:线程池中允许的最大线程数。 - keepAliveTime:线程空闲后的存活时间。 - unit:keepAliveTime 的时间单位。 使用 ThreadPoolExecutor 的步骤如下: 1. 创建 ThreadPoolExecutor 对象 ```java ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); ``` 2. 提交任务给线程池 ```java threadPool.execute(new Runnable() { @Override public void run() { // 任务代码 } }); ``` 3. 关闭线程池 ```java threadPool.shutdown(); ``` 以下是一个使用 ThreadPoolExecutor 的示例代码: ```java import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadPoolDemo { public static void main(String[] args) { int corePoolSize = 5; int maximumPoolSize = 10; long keepAliveTime = 500; TimeUnit unit = TimeUnit.MILLISECONDS; ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); for (int i = 0; i < 20; i++) { threadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } threadPool.shutdown(); } } ``` 在上面的代码中,我们创建了一个线程池,核心线程池大小为 5,最大线程池大小为 10,任务队列大小为 100。然后我们提交了 20 个任务给线程池,每个任务执行 1 秒钟。最后我们关闭了线程池。 注意:在使用 ThreadPoolExecutor 时,需要合理地设置参数,以充分利用系统资源,避免线程池阻塞或者资源浪费。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值