线程池的使用场景
- 高并发、任务执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
- 并发不高、任务执行时间长的业务要区分开看:
- 假如是业务时间长集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占用CPU,所以不要让所有的CPU闲下来,可以加大线程池中的线程数目,让CPU处理更多的业务
- 假如是业务时间长集中在计算操作上,也就是计算密集型任务,线程池中的线程数设置得少一些,减少线程上下文的切换
- 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考上面。最后,业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦
线程池的作用
线程使应用能够更加充分合理的协调利用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)
表示执行拒绝策略的对象。当要创建的线程数量大于线程池的最大线程数,并且缓存队列也满了的时候,新的任务就会被拒绝,就会调用这个接口里的这个方法。这是一种简单的限流保护。友好的拒绝策略可以使如下三种:
- 保存到数据库进行削峰填谷。在空闲的时候再拿出来执行。
- 转向某个提示页面。
- 打印日志。
如何来设置ThreadPoolExecutor的参数
需要根据几个值来决定
- tasks :每秒的任务数,假设为500~1000
- taskcost:每个任务花费时间,假设为0.1s
- 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,那么就新增线程来处理任务;
- 如果缓存队列已满,并且当前线程数目也已经达到上限,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务,调用拒绝策略。