线程池
池的意义
在资源不足以满足当前需求时,将资源圈定为池,反复利用(计划经济),从而满足当前需求,从本质上来讲是一种以时间换空间的做法。
线程池的意义
线程池,是在存在大量任务的前提下,固定一定数量的线程去执行这些任务;提高了线程本身的利用率,减少了大量创建销毁线程所带来的性能开销问题。、
线程池解决的问题
- 反复创建线程开销大 -> 线程池只创建少量线程并反复利用去执行任务、
- 过多的线程会占用较大的进程内存 -> 线程池同时存货的线程只有少量
线程池的好处
- 合理利用资源(CPU和内存)
- 统一管理资源
- 加快程序的响应速度(减少线程创建 -> 减少资源开销 -> 加快程序执行速度)
线程池的创建和停止
线程池的创建
构造函数参数
- corePoolSize:核心线程数,默认情况下,线程池中存活的最少线程数量,但是如果
allowCoreThreadTimeout
设置为true
,那么核心线程在空闲时也会被回收 - maxPoolSize:最大线程数,在核心线程和任务存储队列都工作饱和的情况下,线程池会再创建线程,此时线程池中线程上限为最大线程数;
workQueue
非无界队列时该参数才有意义 - keepAliveTime:非核心空闲线程的保持存活时间,目的是回收空闲的非核心线程,保持线程池的线程数量,减少线程池资源消耗
- workQueue:任务存储队列,如果核心线程都在执行任务,那么会先把任务放入workQueue中,等待线程池线程来执行
- threadFactory:线程创建的工厂,默认工厂Executors.defaultThreadFactory创建出的线程都在同一个线程组,有同样的优先级且都不是守护线程
- handler:线程拒绝策略,当线程池已经关闭,或核心线程、非核心线程和工作队列都已满时,对新提交的工作任务执行的拒绝策略
线程池创建方法的选择
- 手动创建:在明确业务场景的情况下最好是手动创建线程池,可以让我们更明确当前线程池的运行规则,避免资源耗尽等风险
- 自动创建:方便,但是有各自的危害
- FixedThreadPool:core = max,但是workQueue是LinkedBlockingQueue,那么在线程处理速度赶不上任务提交速度时,极有可能OOM
- SingleThreaPool:core = max = 1,workQueue为LinkedBlockingQueue,和Fix一样,可能OOM
- CachedThreadPool:该线程池和上述两个线程池不同,它的主要风险点在于maxPoolSize,该参数被设置为Integer.MAX_VALUE,且因为workQueue为SynchronousQueue,只中转任务,最后会导致线程一直被创建,最终OOM
- SheduleThreadPool:该线程池的主要问题在于参数workQueue,它的容量只有16,但是以150%的比率进行resize,上限为Integer.MAX_VALUE,如果被不断提交一些执行时间/周期较长的任务,在线程数量或队列容量方面都有可能导致OOM,且workQueue导致maxPoolSize无意义
线程添加规则
- 如果线程数小于corePoolSize,那么在新任务的情况下,即使存在空闲线程,也会创建新线程来执行任务
- 如果线程数等于/大于corePoolSize,但是小于maxPoolSize,且任务存储队列未满,那么会将新任务放入任务存储队列中
- 如果线程数等于/大于corePoolSize,但是小于maxPoolSize,且任务存储队列已满,则创建新线程执行任务
- 如果线程数等于/大于maxPoolSize,且任务存储队列已满,则对新任务执行线程拒绝策略
线程增减特性
- coorPoolSize = maxPoolSize,即固定线程数的线程池
- 线程池希望保持较小的线程数,所以只在必要时(负载变得很大)才增加线程数
- 通过提高设置maxPoolSize,比如Integer.MAX_VALUE,可以让线程池处理任意数量的并发任务
- 通过设置workQueue为无界队列,可以让线程池容纳任务数量的任务
常见工作队列
- SynchronousQueue:直接交接队列,对工作任务只做中转,不存储任务,会阻塞任务提交线程/任务处理线程
- LinkedBlockingQueue:无界队列,如果线程处理速度小于任务提交速度,会导致队列中任务一直增加,最终造成OOM;该队列让maxPoolSize无效
- ArrayBlockingQueue:有界队列,可以在生成线程池时提前设置好队列大小,是比较常用的一种工作队列
线程数量的计算
- CPU密集型任务:比如加密、计算等相关任务,这些任务会频繁的使用CPU,那么此时我们可以直接将线程数量设置为CPU数量的一至两倍,最大程度上利用CPU资源
- IO密集型任务:比如读写数据库、读取文件、网络读写等相关任务,这些任务会经常在等待某些资源,那么线程数可以设置为CPU的很多倍,保证CPU不会处在等待期间内,具体的一个数量可以通过压测得出
Brain Goetz的计算公式:线程数 = CPU核心数 * (1 + 平均等待时间 / 平均工作时间)
,平均等待时间
代表CPU的等待时间,平均工作时间
代表CPU的工作时间,前者越大说明CPU需要等待的时间越长,可以把线程数量设置的大一些,尽量不让CPU空闲;后者越大则说明使用CPU的时间越长,线程数量需要设置的小一些,因为CPU已经满负荷运转了,设置再多的线程也只是徒劳,只会给内存增加压力
线程池关闭的相关方法
- shutdown:该方法仅仅是初始化整个关闭过程,会将正在执行的任务和队列中的任务执行完,对新任务会执行rejectHandler
- isShutdown:判断线程池是否被标记为关闭状态
- isTerminated:判断整个线程池是否已经完全停止了,这里的完全停止意味着正在执行和任务队列中的任务都已完成,线程池已关闭
- awaitTermination:检测线程池是否完全关闭,会根据传入的时间参数阻塞调用线程
- shutdownNow:暴力结束线程池,会对正在执行任务的线程调用
interrupt()
,并以List<Runnable>
的形式返回任务队列中的所有任务,但是需要注意,这并不意味着该线程池已经terminated,有些线程还在执行中,因为interrupt()
方法不会立刻停止线程
线程池的拒绝策略
拒绝时机
- 线程池已经被关闭
- maxPool和workQueue都已满
拒绝策略
- AbortPolicy:抛出RejectExecutionException,相当于一个通知去告知任务提交方,线程池不执行这个任务
- DiscardPolicy:默默丢弃任务,不通知任务提交方
- DiscardOldestPolicy:抛弃工作队列中最老的任务(一般为队列头),把新提交的任务加入工作队列中
- CallerRunsPolicy:让任务提交方去执行当前任务
线程池的钩子
钩子,即任务执行前后进行的一些操作,相当于给线程池加了个切面,这是ThreadPoolExecutor
中提供的一些方法,可以通过重写去定义相关操作
- beforeExecution
- afterExecution
线程池的组成部分
线程池主要由以下四部分组成
- 线程池管理器
- 工作线程
- 工作队列
- 任务接口
线程池中线程的复用
线程池的线程,比如corePool中的线程,都是已经被调用过start()
,这些线程的作用(run()
)是,不断的去任务队列中获取任务,然后直接调用任务的run()
,让任务执行
线程池的状态
- RUNNING:接受新任务并执行
- SHUTDOWN:不接受新任务,但是会将线程正在执行的和任务队列中的任务执行完,调用
shutdown()
后线程池会处在该状态 - STOP:不接受新任务,泛起并返回工作队列中所有未执行任务,并调用正在执行任务的线程的
interrupt()
方法,调用shutdownNow()
后线程池会处在该状态 - TIDYING:任务队列为空,工作线程数为0,该状态后线程池就会调用
terminated()
方法真正终止线程池 - TERMINATED:
terminated()
方法运行完成后,线程池转变到该状态
使用线程池的注意点
- 避免任务堆积
- 避免线程数过度增加
- 避免线程泄露,即因为任务逻辑或某些异常原因,导致线程池中的线程数量和预期的不符