使用线程池主要为了解决一下几个问题:
通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。
对线程进行一些维护和管理,比如定时开始,周期执行,并发数控制等等。
一、Executor接口关系
Executor是一个接口,跟线程池有关的基本都要跟他打交道。下面是常用的ThreadPoolExecutor的关系。
- Executor:接口很简单,只有一个execute方法。
- ExecutorService:是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。
- AbstractExecutorService:是一个抽象类。ThreadPoolExecutor就是实现了这个类。
- ThreadPoolExecutor:是创建线程池的核心类,对核心方法进行了实现。
- Executors :没有任何继承关系,对ThreadPoolExecutor的再封装,定义了newFixedThreadPool(),newSingleThreadExecutor()等
二、多种线程池创建方式:
- Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
- Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
- Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
- Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
- Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
- ThreadPoolExecutor:最原始的创建线程池的方式。
三、ThreadPoolExecutor机制
1. 线程池状态
线程池的状态要和线程的状态区分开来,初学者可能会记混淆。
线程池的状态运行状态共有5种,分别是:
- RUNNING:运行中, 为初始状态,即刚创建的线程池就是此状态。
- TERMINATED,终止状态,钩子函数terminated()已经执行完成,线程池彻底销毁。
- TIDYING:清理中,所有任务都停止了,且线程数量也降为0。
- STOP:停止状态,不再接收新任务,不再处理已有任务,且会中断正在执行的任务。
- SHUTDOWN:停工状态,不再接收新任务,但会继续处理队列中的任务。
线程的状态对比着了解:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。(线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态,进入同步队列)
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。(通过调用线程的wait()方法,让线程等待某工作的完成,进入等待队列)
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。(通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 )
- 终止(TERMINATED):表示该线程已经执行完毕。
2. 核心构造方法讲解
2.1构造方法参数讲解
参数名 | 作用 |
corePoolSize | 核心线程数大小 |
maximumPoolSize | 最大线程数大小 |
keepAliveTime | 线程池中超过corePoolSize数目的空闲线程最大存活时间;可以allowCoreThreadTimeOut(true)使得核心线程有效时间 |
TimeUnit | keepAliveTime时间单位 |
workQueue | 阻塞任务队列 |
threadFactory | 新建线程工厂 |
RejectedExecutionHandler | 当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理 |
2.2 任务队列:
线程池中的任务队列.常用的有三种队列,SynchronousQueue ,LinkedBlockingDeque ,ArrayBlockingQueue。
2.3 threadFactory:
线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法
public interface ThreadFactory {
Thread newThread(Runnable r);
}
通过线程工厂可以对线程的一些属性进行定制。
2.4拒绝策略:
RejectedExecutionHandler:RejectedExecutionHandler也是一个接口,只有一个方法
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);
}
当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法。
jdk默认提供了四种拒绝策略:
- CallerRunsPolicy - 调用线程执行,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
- AbortPolicy - 终止执行,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
- DiscardPolicy - 直接丢弃,其他啥都没有
- DiscardOldestPolicy - 丢弃老的任务,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入
3.主要流程:
其中比较容易让人误解的是:corePoolSize,maximumPoolSize,workQueue之间关系。
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
【当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。】
4.参数配置考虑
- CPU密集型任务,线程池的大小可以设置为CPU核心数加1(N+1N+1)或CPU核心数(NN)。这种设置考虑到了任务对CPU资源的消耗,以及一个额外的线程用于防止线程偶发的缺页中断或其他原因导致的任务暂停。当线程数量太小,大量请求可能被阻塞在线程队列中等待执行,导致CPU资源未得到充分利用;而线程数量太大,过多的上下文切换会增加执行时间,影响整体执行效率。
- IO密集型任务,线程池的大小可以设置为2倍的CPU核心数加1(2N+12N+1)或2倍的CPU核心数(2N2N)。这种设置适应于IO密集型任务中,系统大部分时间用于处理IO交互,而线程在处理IO的时间内不会占用CPU。通过增加线程数,可以更好地利用CPU资源,同时保持IO设备的忙碌状态。
- 实际配置时,除了考虑任务类型和服务器配置外,还需要考虑业务场景的具体需求,如并发量、一次业务的整体耗时等。通过测试和调整,找到一个合适的线程池大小,以达到最佳的性能和资源利用率。
- 环境因素也是影响线程池大小设置的重要因素。例如,如果主机上已经运行了其他线程或进程,如Tomcat容器、数据库连接池等,这些都会占用CPU资源,因此在设置线程池大小时需要考虑到这些因素。
注意点:
1、用ThreadPoolExecutor自定义线程池,看线程是的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM
2、如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
3、最大线程数一般设为2N+1最好,N是CPU核数
4、核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数 。
5、如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果