一、为什么要使用线程池
1. 降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁的消耗
2. 提高响应速度
任务不需要等待线程创建就会立即执行,假设一个任务完成的时间是t=t1(线程创建的时间)+ t2(执行任务的时间)+ t3(线程销毁的时间)。线程池缩短了t1,t3的时间,提高了服务器的性能
3. 提高线程的可管理性
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
二、ThreadPoolExecutor的类关系
1. Executor
是一个接口,是Executor框架的基础,它将任务的提交与任务的执行分离开来
2. ExecutorService
继承了Executor,在其上做了一些shutdown(),submit()的扩展,可以说是真正的线程池接口
3. AbstractExecutorService
抽象类实现了ExecutorService接口中的大部分方法
4. ThreadPoolExecutor
线程的核心类,用来执行被提交的任务
5. ScheduledExecutorService
继承了ExecutorService,提供了带周期执行的功能的ExecutorService
6. ScheduledThreadPoolExecutor
在给定的延迟时间后运行命令,或者定期执行命令
三、线程池各参数的含义
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
1. corePoolSize
线程池的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize
如果当前线程数为corePoolSize,继续提交的任务会保存到阻塞队列中,等待被执行
如果执行了线程池的prestartAllCoreThreads,线程池会提前创建并启动所有的核心线程
2. maximumPoolSize
线程池允许的最大线程数,如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize
3. keepAliveTime
线程空闲时的存活时间,即当线程没有执行任务时,继续存活的时间,默认情况下,改参数只有在corePoolSize生效时才有用
4. timeUnit
KeepAliveTime的时间单位
5. workQueue
必须是BlockQueue阻塞队列,一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程造成如下影响
- 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,线程数不会超过corePoolSize
- 由于1,使用无界队列时maximumPoolSize将是一个无效参数
- 由于1和2的原因,参数KeepAliveTime也是一个无效的参数
- 使用无界队列可能会耗尽资源,有界队列则有助于防止资源耗尽,同时,即使使用有界队列,也要控制有界队列的大小在一个合适的范围内
6. threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个线程设置一个具有识别度的线程名,当然还可以更加自由的对线程进行更多的设置
7. rejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交一个任务,必须采用一种策略处理改任务,线程池提供了四种策略
7.1 AbortPolicy
直接抛出异常,默认策略
7.2 CallerRunsPolicy
用调用者所在的线程执行任务
7.3 DiscardOldestPolicy
丢弃阻塞队列中靠最前的任务,并执行当前任务
7.4 DiscardPolicy
直接丢弃任务
7.5 自定义饱和策略
实现RejectdExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
四、线程池的工作机制
- 如果当前运行的线程少于corePoolSize,则创建新的线程执行任务
- 如果运行的线程等于或多余corePoolSize,则将任务加入BlockingQueue
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来执行任务
- 如果创建新的线程将使当前线程超过maximumPoolSize,任务将被拒绝,并调用RejectedExecutionhandler.rejectedExecution()
五、提交任务
1. execute()
用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功
2. submit()
用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
六、关闭线程池
可以调用线程池的shutdown或shutdownNow方法来关闭线程池。原理是遍历线程池中的工作线程,然后依次调用线程的Interrupt方法来中断线程,
七、合理配置线程池
要想合理的配置线程池,就必须首先分析任务特性
- 任务的性质:CPU密集型任务,IO密集型任务,混合型任务
- 任务的优先级:高,中,低
- 任务的执行时间:长,中,短
- 任务的依赖性:是否依赖其他系统资源,如数据库连接
1. CPU密集型任务
和内存打交道,大量计算,例如大数的计算,正则匹配
如何配置:
- CPU密集型任务应该配置尽可能小的线程,如配置Ncpu+1个线程的线程池(Ncpu是处理器核的数目),留一个空出来做切换
- 如果线程太多,会造成线程在CPU内部的上下文切换,CPU线程上下文切换比指令耗时的更多
通过Runtime.getRuntime().availableProcessors()方法获得当前设备的 CPU 个数
2. IO密集型任务
和磁盘,网络,文件,数据库交互很多的
如何配置:
由于IO密集型任务并不是一直在执行的,在操作的时间是不会消耗CPU的,不会经常在CPU内进行上下文切换,则应配置更多的线程,如:2Ncpu
对于IO型的任务的最佳线程数,有个公式可以计算:
NThreads = NcpuUcpu*(1+w/c)
- Ncpu:是处理器核的数目
- Ucpu:是期望cpu利用率(0-1之间)
- w/c:是等待时间与计算时间的比例,等待时间与计算时间我们在Linux下使用相关的vmstat命令或者top命令可以查看
3. 混合型任务
前两者的混合
如何配置:
如果可以拆分,将其拆分为一个CPU密集型任务和一个IO密集型任务,
- 只要这两个任务执行时间相差不大,那么分解后执行的吞吐量将高于串行执行的吞吐量
- 如果这两个任务执行的时间相差太大,则没必要拆分