JUC高级多线程_08:线程池的具体介绍与使用
Posted ABin-阿斌
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC高级多线程_08:线程池的具体介绍与使用相关的知识,希望对你有一定的参考价值。
我是 ABin-阿斌:写一生代码,创一世佳话,筑一揽芳华。 如果小伙伴们觉得我的文章有点 feel ,那就点个赞再走哦。
1 . 简介
1. 线程池的特点:
- 线程池做的工作主要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量 超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
- 主要特点: 线程复用、控制最大并发数、管理线程。
2. 线程池的优点:
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
2 . 使用
1. 架构
- Java 中的线程池是通过 Executor 框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类,主要使用 ExecutorService 接口
2. 线程池的三大方法:
-
Executors.newFixedThreadPool(int)
- 是用于执行长期任务性能好,有固定的线程数,创建一个线程池,一池有N个固定的线程
-
Executors.newSingleThreadExecutor()
- 一个任务一个任务的执行,一池一线程
-
Executors.newCachedThreadPool()
- 执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强
public class JUC10_ThreadPool {
public static void main(String[] args) {
// 1. 一 个线程池受理 五 个线程
// ExecutorService threadpool = Executors.newFixedThreadPool(5);
// 2. 一 个线程池受理 一 个线程
// ExecutorService threadpool = Executors.newSingleThreadExecutor();
// 3. 一 个线程池受理 N 个线程,可扩容
ExecutorService threadpool = Executors.newCachedThreadPool();
try {
//模拟有 10 个顾客来办理业务,但只有 5 个办理窗口
for (int i = 1; i <= 10; i++) {
threadpool.execute(()->{
System.out.println("当前"+ Thread.currentThread().getName()+"办理业务");
});
//模拟办理所需时间
// try {
// TimeUnit.MILLISECONDS.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
}
}
3 . 底层源码
- 由图可知上述三个 API 底层调用的都是同一个方法 —— threadPoolExecutor
- 第五个参数表示一个阻塞队列:
- LinkedBlockingQueue: 由链表结构组成的有界(但大小默认值为 integer.MAX_VALUE)阻塞队列。
- SynchronousQueue: 不存储元素的阻塞队列,也即单个元素的队列。(生产一个,消费一个)
4 . threadPoolExecutor 7 个重要参数
1. int corePoolSize:(核心线程池大小)
- 线程池中的常驻核心线程数,最少存在的线程数
2. int maximumPoolSize:(最大核心线程池大小)
- 线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3. long keepAliveTime:(超时了没有人调用就会释放)
- 多余的空闲线程的存活时间
- 当前池中线程数量超过 corePoolSize 时,且当空闲时间达到 keepAliveTime 时,多余线程会被销毁直到只剩下 corePoolSize 个线程为止
4. TimeUnit unit:(超时单位)
- keepAliveTime 的单位 —— 秒 / 毫秒 / 微秒
5. BlockingQueue workQueue:(阻塞队列)
- 任务队列,被提交但尚未被执行的任务
6. threadFactory threadFactory:(线程工厂:创建线程的,一般 不用动)
- 表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7. RejectedExecutionHandler handler:(拒绝策略)
- 拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
请求执行的 runnable 的策略
5 . 底层原理
- 在创建了线程池后,开始等待请求。
- 当调用 execute() 方法添加一个请求任务时,线程池会做出如下判断:
- 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入 workQueue队列;
- 如果这个时候 workQueue队列 满了且正在运行的线程数量还小于 maximumPoolSize,那么还是要创建非核心线程立刻运行 workQueue队列 中的任务;
- 如果 workQueue队列 满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会 启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从 workQueue队列 中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
- 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
- 所以线程池的所它最终会收缩到 corePoolSize 的大小。
6 . 线程池的选择
- 在工作中单一的、固定数的、可变的,三种创建线程池的方法哪个用的多?及为什么?
- 答 : 一个都不用
- 我们可以看看 《阿里巴巴手册》 给我们的建议:
7 . 自定义线程池
1. 代码示例
public static void main(String[] args) {
ExecutorService threadpool = new ThreadPoolExecutor(2,5, 2L,TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
// 改变任务数,观察输出结果
//模拟有 N 个顾客来办理业务
for (int i = 1; i <= 5; i++) {
threadpool.execute(()->{
System.out.println("当前\\t"+ Thread.currentThread().getName()+"\\t办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadpool.shutdown();
}
}
-
任务队列(workQueue)的参数如果不写就默认为 Integer.MAX_VALUE,所以需要写上
-
线程工厂使用默认的,模仿源码
-
拒绝策略也暂时使用和默认的,下文具体阐述
2. 改变任务的数量
-
情况一:5 个任务
- 不会报错,正常执行,两个线程处理完成
-
情况二:8 个任务
- 不会报错,正常执行,五个线程处理完成
-
情况三:9 个任务
- 会报错,会报 RejectedExecutionException
-
总结:
- 线程池的最多可以容纳数 =【最大线程数(maximumPoolSize) + 任务队列(workQueue)可容纳数】
3. 最大线程数的设置规则
-
如果是 CPU 密集型(CPU 用的最多):maximumPoolSize(最大线程数) = CPU核数 + 1
-
注意:在实际开发当中写死参数的方法不可取,原因:每个人的电脑核数不一样。
-
具体优化方案如下代码展示: 采取自动获取 CPU 核数
// 获取电脑的 CPU 核数
int CPU = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = CPU + 1;
- 如果是 IO 密集型 : maximumPoolSize(最大线程数)= CPU核数 / 阻塞系数
8 .拒绝策略(4种)
1. 简介
- 等待队列已经排满了,再也塞不下新任务了,同时,线程池中的 max 线程也达到了,无法继续为新任务服务
- 也即 最多可以容纳数 达到最大
- 这个是时候我们就需要拒绝策略机制合理的处理这个问题
2. 分类(4 种)
-
ThreadPoolExecutor.AbortPolicy()
- 解释 : 当线程池中任务数量超出 最多可容纳数 时,会直接抛出 RejectedExecutionException 异常 阻止系统正常运行
-
Executor 默认的策略
-
ThreadPoolExecutor.CallerRunsPolicy
- 调用者运行 一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
-
举例 :
- 营业厅A 人多,告诉顾客去 营业厅B 办理 ,可是顾客来到 营业厅B 时, 营业厅B 人更多,便告诉顾客再去 营业厅A 看看。 —— 即不放弃任何一个顾客 ; 营业厅B 把顾客 回退 给 营业厅A
-
ThreadPoolExecutor.DiscardOldestPolicy
- 抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
-
ThreadPoolExecutor.DiscardPolicy
- 该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。
以上是关于JUC高级多线程_08:线程池的具体介绍与使用的主要内容,如果未能解决你的问题,请参考以下文章
JUC高级多线程_10:Future异步回调的具体介绍与使用