线程池的好处
- 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能执行;
- 提高线程的可管理性:线程是稀缺资源,不能无限创建,否则会消耗系统资源、降低系统的稳定性,使用线程可以进行统一分配,调优和监控
线程池的创建
jdk自带创建线程池的四种常见方式:
- Executors.newFixedThreadPool(int):创建一个固定线程数量的线程池,可控制线程最大并发数,超出的线程需要在队列中等待。注意它内部corePoolSize和maximumPoolSize的值(就是第一和第二个参数 nThreads)是相等的,并且使用的是LinkedBlockingQueue:
-
源码:
1
2
3
4
5
public
static
ExecutorService newFixedThreadPool(
int
nThreads) {
return
new
ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new
LinkedBlockingQueue<Runnable>());
}
2. Executors.newSingleThreadExecutor():创建一个单线程的线程池,它只有唯一的线程来执行任务,保证所有任务按照指定顺序执行。注意它内部corePoolSize和maximumPoolSize的值都为1,它使用的是LinkedBlockingQueue:
源码:
1 2 3 4 5 6 |
|
3. Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则创建新线程。注意它内部将corePoolSize值设为0,maximumPoolSize值设置为Integer.MAX_VALUE,并且使用的是SynchronizedQueue,keepAliveTime值为60,即当线程空闲时间超过60秒,就销毁线程:
源码:
1 2 3 4 5 |
|
4. Executors.newScheduledThreadPool(int):创建一个固定线程数量的线程池,相比于newFixedThreadPool(int)固定个数的线程池强大在 ①可以执行延时任务,②也可以执行带有返回值的任务,并且使用的是DelayedWorkQueue:
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
注意:
- 以上四种创建线程的方式内部都是由ThreadPoolExecutor这个类完成的,该类的构造方法有5个参数,称为线程池的5大参数(还有另外两个参数);
- 线程池使用完毕之后需要关闭,应该配合try-finally代码块,将线程池关闭的代码放在finally代码块中;
线程池的7大参数
ThreadPoolExecutor对构造函数进行了重载,实际内部使用了7个参数:
1 2 3 4 5 6 7 8 9 10 |
|
- corePoolSize:线程池中常驻核心线程池(当线程池中的线程数目达到了corePoolSize后,就会把任务放到缓存队列中;)
- maximumPoolSize:线程池中能够容纳同时执行最大线程数,该值必须大于等于1
- keepAliveTime:多余线程的最大存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务(阻塞队列)
- threadFactory:生成线程池中工作线程的线程工厂,一般使用默认即可
- handler:拒绝策略,表示当任务队列满并且工作线程大于等于线程池的最大线程数时,对即将到来的线程的拒绝策略
线程池底层原理
线程池具体工作流程:
- 在创建线程后,等待提交过来的任务请求
- 当调用execute()/submit()方法添加一个请求任务时,线程池会做出以下判断:
- 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务
- 如果正在运行的线程数量大于等于corePoolSize,会将该任务放入阻塞队列中
- 如果队列也满但是正在运行的线程数量小于maximumPoolSize,线程池会进行拓展
- 将线程池中的线程数拓展到最大线程数
- 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程空闲时间超过给定的keepAliveTime时,线程会做出判断:
- 如果当前运行线程大于corePoolSize,那么该线程将会被停止。也就是说,当线程池的所有任务都完成之后,它会收缩到corePoolSize的大小
线程池的拒绝策略
当线程池的阻塞队列满了同时线程池中线程数量达到了最大maximumPoolSize时,线程池将会启动相应的拒绝策略来拒绝请求任务。
4种拒绝策略具体为:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案
注意:以上4种拒绝策略均实现了RejectedExecutionHandler接口
规范创建线程池
实际开发中不允许使用内置的线程池:必须明确地通过ThreadPoolExecutor方式,指定相应的线程池参数创建自定义线程或者使用其它框架提供的线程池。因为内置线程池的第五个参数阻塞队列允许的请求队列长度为 Integer.MAX_VALUE(从上面的源码上可以看出),可能造成大量请求堆积,导致OOM:
阿里巴巴规范中指出不能使用Executors去创建:
自定义线程池:使用不同的拒绝策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
线程池配置合理线程数量
线程池合理配置线程数量需要考虑业务具体是CPU密集型还是IO密集型:
- CPU密集型:该任务需要大量运算,而没有阻塞,CPU一直在全速运行,CPU密集型只有在真正的多核CPU上才能进行加速。
CPU密集型任务配置应该尽可能少的线程数量,一般公式为:
1 |
|
- IO密集型:任务需要大量的IO操作,即大量的阻塞。在单线程上进行IO密集型的任务会浪费大量的CPU运算能力在等待操作上。
所以在IO密集型任务中使用多线程可以大大加速程序运行:
1 2 |
|
面试题
1. 如果线程池中使用了无界阻塞会发上什么问题?
调用超时,队列会变得越来越大,此时内存飙升,而且还会导致OOM内存溢出。
2.如果线程池的队列满了以后,会发生 什么事情?
如果队列无界则会导致内存溢出,如果是有界队列,就会被配置的拒绝策略所拒绝。
建议:自定义一个reject策略,如果线程池无法执行更多的任务,此时可以把任务信息持久化写入磁盘中,后台专门启动一个线程,后续等待线程池的工作负载降低了,再从磁盘慢慢读取之前持久化的任务,重新提交到线程池去执行。
3. 线上机器突然宕机,线程池的阻塞队列中的请求怎么办?
如果没有做持久化,必然会丢失的,所以防止这种情况就要持久化到数据库,使用状态来区分是否执行,等待服务器重启,重新扫描任务进而去执行。