线程池介绍
一、线程池
线程池就是把一堆线程提前创建好了,放在池子里,当需要用到的时候可以直接使用,使用完以后会把线程放回池子里。
线程池的优势:
- 低消耗,降低了创建线程和销毁线程的开销
- 提高响应的速度(因为不需要创建线程)
- 提高线程的可管理性,可以统一管理线程
线程池的配置
如果线程池中线程的数量过多,最终它们会竞争稀缺的处理器和内存资源,浪费大量的时间在上下文切换上。反之,如果线程的数目过少,正如你的应用所面临的情况,处理器的一些核可能就无法充分利用。线程池大小与处理器的利用率之比可以使用下面的公式进行估算: N(threads) = N(CPU) * U(CPU) * (1 + W/C) 其中: N(CPU)是处理器的核的数目,可以通过Runtime.getRuntime().availableProcessors()得到 U(CPU)是期望的CPU利用率(该值应该介于0和1之间) W/C是等待时间与计算时间的比率
二、线程池状态
状态 | 含义 |
---|---|
RUNNING | 运行状态,该状态下线程池可以接受新的任务,也可以处理阻塞队列中的任务 执行 shutdown 方法可进入 SHUTDOWN 状态 执行 shutdownNow 方法可进入 STOP 状态 |
SHUTDOWN | 待关闭状态,不再接受新的任务,继续处理阻塞队列中的任务 当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态 |
STOP | 停止状态,不接收新任务,也不处理阻塞队列中的任务,并且会尝试结束执行中的任务 当工作线程数为0时,进入 TIDYING 状态 |
TIDYING | 整理状态,此时任务都已经执行完毕,并且也没有工作线程 执行 terminated 方法后进入 TERMINATED 状态 |
TERMINATED | 终止状态,此时线程池完全终止了,并完成了所有资源的释放 |
三、线程池工作原理
当一个任务提交给线程池时。注意:工作队列就有两种实现策略:无界队列和有界队列。无界队列不存在饱和的问题,但是其问题是当请求持续高负载的话,任务会无脑的加入工作队列,那么很可能导致内存等资源溢出或者耗尽。而有界队列不会带来高负载导致的内存耗尽的问题,但是有引发工作队列已满情况下,新提交的任务如何管理的难题,这就是线程池工作队列饱和策略要解决的问题。
- 当线程池接收到一个任务时,如果核心线程数没有达到corePoolSize,那么就会新建一个线程,并绑定该任务,直到工作线程的数量达到 corePoolSize 前都不会重用之前的线程。当核心线程数满了的情况,有新的任务进来,如果核心线程有空闲,则使用核心线程执行任务,如果没有空闲,进行第二步
- 判断当前阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步;
- 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的临时线程来执行任务,否则,则交给饱和策略进行处理
四、线程池参数
线程池线程数选型,仅参考,实际情况比较复杂
CPU密集型:因为CPU密集型任务CPU的使用率很高,若开过多的线程,只能增加线程上下文的切换次数,带来额外的开销
IO密集型:IO密集型CPU使用率不高,可以让CPU等待IO的时候处理别的任务,充分利用cpu时间。IO密集型表示大部分时间花在等待IO上。
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程,根据公式:
最佳核心线程数目
CPU密集型 = 核心线程数 = CPU核数 + 1
IO密集型 = CPU核数 / (1-阻塞系数) 例如阻塞系数 0.8,CPU核数为4
corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,即使当前核心线程池有空闲的线程。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了
prestartCoreThread()
或者prestartAllCoreThreads()
,线程池创建的时候相应的核心线程都会被创建并且启动。建议值为:每秒任务数*任务执行时间(例如0.5s) 【100 * 0.2=20】或者cpu数量+1
maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。
keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
unit:时间单位。为keepAliveTime指定时间单位。
workQueue:阻塞队列。用于保存任务的阻塞队列,可以使用ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue。
threadFactory:创建线程的工厂类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。
handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种:
AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
CallerRunsPolicy:只用调用者所在的线程来执行任务;
DiscardPolicy:不处理直接丢弃掉任务;
DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
五、为什么阿里巴巴规范禁止使用Excutors创建线程池
因为Excutors创建线程池也是调用了ThreadPoolExcutor,只是使用了不同的参数、队列、拒绝策略,如果使用不当会造成资源耗尽(OOM问题),不方便排查。根据实际需要创建可以让使用者更加清楚线程池的允许规则等等,规避一些不必要的风险。
常见线程池的问题:
Executors.newFixedThreadPool(0)和Executors.newSingleThreadExecutor()
队列使用的是LinckedBlockingQueue,队列长度为Integer.MAX_VALUE,当大量请求堆积的时候,容易造成oom
Executors.newCachedThreadPool()和Executors.newScheduledThreadPool(0)
线程池里允许的最大线程数为Integer.MAX_VALUE,会创建过多线程,造成oom。创建线程时用的内存并不是jvm堆内存,而是系统的剩余内存