文章目录
- 前言
- 一、Java线程池基本概念
- 二、Java线程池如何使用?
- 三、Java线程池的七个重要参数
- 四、线程池底层原理与处理流程
- 五、自定义线程池
- 六、如何合理的配置线程池参数,比如最大线程数?
前言
对Java线程池的学习,予以记录!
一、Java线程池基本概念
1、为什么要使用线程池?
降低了之前通过new Thread对象频繁创建与销毁线程所带来的资源损耗
2、线程池是干嘛的?
主要用来控制运行的线程的数量,处理过程中将任务放入队列中,然后在线程创建后启动这些任务,如果线程的数量超过了最大数量的线程则排队等候,等其他线程执行完毕,再从队列中取出任务来执行
3、使用线程池有什么优势?
①线程复用
降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗
②控制最大并发数
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行
③管理线程
提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
二、Java线程池如何使用?
1、架构说明
Java中的线程池是通过Executor框架实现的,该框架用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类
2、编码实现
五种从线程池创建线程方式(1,2了解,3,4,5掌握)
通过分别调用Executors类的方法实现不同类型功能的线程池,但是底层仍然是调用了ThreadPoolExecutor
// 创建一个定长线程池,支持定时及周期性任务执行。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10);
// java8新出
ExecutorService executor = Executors.newWorkStealingPool();
// 适用:执行长期的任务,性能好很多。1线程池5个处理线程
ExecutorService executor = Executors.newFixedThreadPool(5);
// 适用:一个任务一个任务的执行的场景。1线程池1个处理线程
ExecutorService executor = Executors.newSingleThreadExecutor();
// 适用:执行很多短期异步的小程序或者负载较轻的服务器。1线程池N个处理线程
ExecutorService executor = Executors.newCachedThreadPool();
①Executors.newScheduledThreadPool(常驻核心线程数)
创建一个定长线程池,支持定时及周期性任务执行。延迟执行
②Executors.newWorkStealingPool()
java8新特性
,创建一个具有抢占式操作的线程池ForkJoinPool 类
③Executors.newFixedThreadPool(线程数)
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
④Executors.newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
⑤Executors.newCachedThreadPool()
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
阻塞队列的学习!!!
3、Demo:线程池的简单使用
public class ThreadPoolExecutorDemo {
/*10个人在银行办事,银行有五个工作人员*/
public static void main(String[] args) {
// ExecutorService executor = Executors.newFixedThreadPool(5);
// ExecutorService executor = Executors.newSingleThreadExecutor();
ExecutorService executor = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 10; i++) {
int finalI = i;
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t" + finalI);
});
}
}
finally {
executor.shutdown();
}
}
}
结果:不唯一
三、Java线程池的七个重要参数
1、corePoolSize:线程池中的常驻核心线程数
表示线程池核心线程数,当初始化线程池时,会创建核心线程进入等待状态,即使它是空闲的,核心线程也不会被摧毁,从而降低了任务一来时要创建新线程的时间和性能开销。
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 在线程池中的线程数达到corePoolSize后,就会把到达的任务放到缓存队列中
2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
表示最大线程数,意味着核心线程数都被用完了,那只能重新创建新的线程来执行任务,但是前提是不能超过最大线程数量,否则该任务只能进入阻塞队列进行排队等候,直到有线程空闲了,才能继续执行任务。
3、keepAliveTime:多余的空闲线程的存活时间
表示线程存活时间,除了核心线程外,那些被新创建出来的线程可以存活多久。意味着,这些新的线程一但完成任务,而后面都是空闲状态时,就会在一定时间后被摧毁。
4、unit:keepAliveTime的单位
存活时间单位。
5、workQueue:任务队列,被提交但尚未被执行的任务
表示任务的阻塞队列,由于任务可能会有很多,而线程就那么几个,所以那么还未被执行的任务就进入队列中排队,队列我们知道是 FIFO 的,等到线程空闲了,就以这种方式取出任务。这个一般不需要我们去实现。
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
7、handler:拒绝策略,表示当前队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)
等待队列已经排满了,再也塞不下新任务了,同时,线程池中的线程数量也达到了maximumPoolSize,无法继续为新任务服务,此时我们就需要拒绝策略机制合理的处理这个问题
JDK内置的四种拒绝策略均实现了RejectedExecutionHandler接口
①AbortPolicy(默认)
直接抛出RejectedExecutionException异常阻止系统正常运行
②CallerRunsPolicy
“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
③DiscardOldestPolicy
抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
④DiscardPolicy
直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
四、线程池底层原理与处理流程
1、线程池底层是什么?
ThreadPoolExecutor、ThreadPoolExecutor、ThreadPoolExecutor
重要的事情说三遍!!!
除WorkStealingPool底层是具有抢占式操作的ForkJoinPool类,其余四种线程池ScheduledThreadPool,FixedThreadPool,SingleThreadExecutor以及CachedThreadPool的底层都是ThreadPoolExecutor
2、线程池处理流程
- 在创建了线程池后,等待提交过来的任务请求。
- 当调用execute()方法添加一个请求任务时,线程池会做如下判断:
- 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
- 如果这时候队列满了且正在运行的线程数量还小于maximumPooSize,那么还是要创建非核心线程立刻运行这个任务;
- 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
- 当一个线程完成任务时,它会从队列中取下一个任务来执行。
- 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
- 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小。
五、自定义线程池
1、为什么要自定义线程池?
因为线程资源必须通过线程池提供,不允许在应用中自行显式创建线程(new Thread().start())
2、为什么不用JDK帮助我们已经写好的Executor类的new方法?
一个都不用,生产中我们只使用自己定义的
生产实践与知识教育并不能一概而论
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险:
- newFixedThreadPool和newSingleThreadExecutor:允许的LinkedBlockingQueue请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
- newCachedThreadPool和newScheduledThreadPool:允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
3、Demo验证:线程池的处理流程及拒绝策略
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
for (int i = 1; i <= 5; i++) {
threadPool.execute(() -> {
System.out.println("线程" + Thread.currentThread().getName() + "\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
循环次数等于corePoolSize + 阻塞队列长度时,不创建新的线程:
将循环次数改为8,大于corePoolSize + 阻塞队列长度且线程数小于maximumPoolSize时,需要创建新的线程:
循环次数改为9,大于maximumPoolSize + 阻塞队列长度时:
- 在AbortPolicy拒绝策略下出现RejectedExecutionException异常:
- 在CallerRunsPolicy拒绝策略下出现调用调用者线程运行的现象:
- 在DiscardOldestPolicy拒绝策略下,抛弃队列中等待最久的任务。当循环次数改为更大时,任务数量始终保持maximumPoolSize + 阻塞队列长度:
- 在DiscardPolicy拒绝策略下,直接丢弃任务,不予任何处理也不抛出异常。当循环次数改为更大时,任务数量始终保持maximumPoolSize + 阻塞队列长度:
4、总结
- 处理流程
- 线程池可处理的任务数 = 最大线程数 + 阻塞队列长度
- 当创建线程到最大线程数后,非核心线程数优先处理不在阻塞队列中的任务。
- 任务数等于corePoolSize + 阻塞队列长度时,不创建新的线程
- 任务数大于corePoolSize + 阻塞队列长度且线程数小于maximumPoolSize时,需要创建新的线程
- 任务数大于corePoolSize + 阻塞队列长度且线程数大于maximumPoolSize时,采取相应的任务拒绝策略
1.当正在运行的线程数小于常驻核心线程数时,那么会立即创建线程执行任务;
2.当正在运行的线程数大于或者等于核心线程数,那么将这个任务放入队列中;
3.当队列已满且正在运行的线程数小于最大线程数,那么还是要创建非核心线程立刻运行非队列中的任务;
4.当队列满了,且运行线程数大于或者等于最大线程数,那么就启用饱和拒绝策略来执行
- 拒绝策略
- 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略
四大拒绝策略 | 作用 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出RejectedExecutionException异常 |
ThreadPoolExecutor.CallerRunsPolicy | 由调用线程(提交任务的线程)处理该任务 |
ThreadPoolExecutor.DiscardOldestPolicy | 丢弃队列等待最久的的任务,然后重新提交当前任务(任务数为maximumPoolSize + 阻塞队列长度) |
ThreadPoolExecutor.DiscardPolicy | 直接丢弃任务,不予任何处理也不抛出异常(任务数为maximumPoolSize + 阻塞队列长度) |
六、如何合理的配置线程池参数,比如最大线程数?
1、查看当前电脑配置–>CPU核心线程数
// 获取cpu核心线程数(计算资源)询问jvm,jvm去问操作系统,操作系统去问硬件
System.out.println(Runtime.getRuntime().availableProcessors());
我的笔记本CPU核心线程数是20
2、根据具体业务情况分析,CPU密集或IO密集
①CPU密集,CPU核心线程数+1个线程的线程池
也就是说,我应该设置最大线程数为20+1=21
②IO密集,CPU核心线程数*2 或 CPU核心线程数/(1-阻塞系数)
设置最大线程数一般根据业务压测出来
也就是说,我可以设置最大线程数为20*2=40或者20/(1-0.9)=200【取乐观阻塞系数,频繁IO操作】