线程池
线程池的优势
- 降低系统资源消耗,可以重用已存在的线程,减少了线程创建和销毁的不必要消耗;
- 提高系统响应速度,当有任务到达时,通过重用已存在线程,可以无需等待新线程的创建;
- 方便管理线程并发数量;
- 还有一些扩展功能,如延时定时线程池
线程池的流程
基础的使用
主要通过Executors的newXXXThreadPool();/newXXXThreadExecutor();
方法来创建线程池(返回ExecutorService对象,ExecutorServcice是Java提供的用于管理线程池的类),通过对象的execute方法调用线程池里的线程执行对应的代码。
// FixedThreadPool(n)代表创建一个内部有固定n个的线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// SingleThreadExecutor()代表创建一个只有一个线程的线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// CachedThreadPool()代表创建可以无限扩大的线程池
ExecutorService executor = Executors.newCachedThreadPool();
newFixedThreadPool方法的源码:
newSingleThreadExecutor方法的源码:
newCachedThreadPool方法的源码:
使用的模板:
public void init() {
// 三种自带的线程池的创建方式
ExecutorService executor2 = Executors.newFixedThreadPool(5);
ExecutorService executor3 = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newCachedThreadPool();
try {
for (int i = 0; i < 10; i++) {
// 用execute来使用线程池中的线程
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " thread running");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 最后需要关闭线程池
executor.shutdown();
}
}
以上的三个方法最终都调用了下面的这个构造方法:
线程池中构造方法中的七大参数的用途
- int corePoolSize :
- 表示线程池中常驻的核心线程数
- int maximumPoolSize:
- 表示线程池中能容纳的同时执行的最大线程数,此值必须>=1
- long keepAliveTime:
- 多余的空闲线程存活时间(线程池中线程数量超过corePoolSize,且空闲时间达到keepAliveTime,多余线程被销毁,到最后只剩下corePoolSize 的数量)
- TimeUnit unit:
- keepAliveTime的单位
- BlockingQueue workQueue:
- 任务队列,存放被提交但是还未执行的任务
- ThreadFactory threadFactory:
- 表示生产线程的线程工厂,一般默认即可
- RejectedExecutionHandler handler:
- 定义拒绝策略,表示当队列满了,且工作线程数大于等于线程池的最大线程数以后如,何拒绝请求执行的runnable
为什么要使用阻塞队列,而不是非阻塞的?
阻塞队列在任务队列中没有任务时,阻塞获取任务的线程,使线程进入wait状态,释放CPU的资源。
当队列中有任务时才会唤醒对应的线程,从队列中取出消息并执行。
线程池底层工作的流程
- 线程池创建,等待请求
- 当调用execute()方法后,添加一个请求任务开始判断:
- 如果当前正在运行的线程数量小于corePoolSize,那么马上调用线程来执行这个任务;
- 如果当前正在运行的线程数量大于等于corePoolSize,且等待队列未满,那么将这个任务加入队列
- 如果此时队列已经满了,且正在运行的线程数量小于maximumPoolSize,那么扩容,即开启非核心线程来运行这个任务;
- 如果队列已满,且运行的线程数量大于等于maximumPoolSize,那么线程池回启动饱和拒绝策略来应对
- 当一个线程完成任务时,从等待队列中取出下一个任务来执行;
- 当一个线程空闲超过keepAliveTime,线程判断:
- 当运行的线程数大于corePoolSize,那么此时这个空闲线程被停止,否则不处理
- 最后线程池所有任务完成后,最终会收缩到corePoolSize的大小。
创建线程池的规范
根据阿里发布的开发手册:
【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这
样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
所以在实际开发时一般会自己创建线程池:
public static void main(String[] args) {
ExecutorService threadPool =
new ThreadPoolExecutor(2, // 常驻核心线程数
Runtime.getRuntime().availableProcessors() + 1, // 最大线程数
2, // 空闲存活的时间
TimeUnit.SECONDS, // 空闲存活时间的单位
new LinkedBlockingQueue<>(3), // 等待队列,一般需要给定队列的大小
Executors.defaultThreadFactory(), // 线程工厂,一般使用默认的
new ThreadPoolExecutor.DiscardOldestPolicy()); // 拒绝策略
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + " thread running... ...");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
注意点:
-
线程池的最大的线程数(maximumPoolSize)一般可以设置为本机的 逻辑处理器数量+1 ,而逻辑处理器的数量可以通过
Runtime.getRuntime().availableProcessors()
来获取; -
存放等待任务的阻塞队列最好赋予初始值,如果使用默认情况,队列的长度会过大,这样拒绝策略相当于无效了,不可取;
-
线程工厂通过
Executors.defaultThreadFactory()
来创建默认工厂; -
拒绝策略通过
new ThreadPoolExecutor.xxxPolicy()
来创建指定
四种拒绝策略:
均发生在当线程全部在使用且阻塞队列满时
1、AbortPolicy()
丢弃要进入的任务,并且抛出RejectedExecutionException异常。
2、CallerRunsPolicy()
交给直接调用此线程的线程来处理要进入的任务。
3、DiscardPolicy()
丢弃要进入的任务,但是不抛出异常,静默丢弃。
4、DiscardOldestPolicy()
丢弃阻塞队列中排在最前面的任务,然后重新提交被拒绝任务。