目录
2.1 Excutors.newFixedThreadPool(int)
2.2 Excutors.newSingleThreadExecutor(int)
2.3 Excutors.newCachedThreadPool(int)
2.4 Excutors.newScheduledThreadPool(int)
2.5 Excutors.newSingleThreadScheduledExecutor(int)
2.6 Excutors.newWorkStealingPool(int)
6.1 自定义线程池(即自己传入参数来new ThreadPoolExcutor)使用案例
1.概述
ThreadPoolExecutor 是线程池的核心实现
2.Excutors创建线程池的6种方式
2.1 Excutors.newFixedThreadPool(int)
- 创建固定线程数的线程池
(1)使用案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);//一个池5个线程
//模拟10个用户来办理业务,每个用户就是来自于外部的请求线程
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
- 可以发现只有5个线程被复用去处理业务
(2)源码及特点
主要特点:
- 1.创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列种等待
- 2.创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue
2.2 Excutors.newSingleThreadExecutor(int)
- 创建只有一个线程的线程池
(1)使用案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();//一个池1个线程
//模拟10个用户来办理业务,每个用户就是来自于外部的请求线程
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
- 可以发现只有一个线程处理任务
(2)源码及特点
主要特点:
- 1.创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
- 2.创建的线程池corePoolSize和maximumPoolSize值都设置为1,它使用的是LinkedBlockingQueue
注意:
newFixedThreadPool(1,threadFactory)
不等价于newSingleThreadExecutor
newSingleThreadExecutor
创建的线程池保证内部只有一个线程执行任务,并且线程数不可扩展;- 而通过
newFixedThreadPool(1, threadFactory)
创建的线程池可以通过setCorePoolSize
方法来修改核心线程数。
2.3 Excutors.newCachedThreadPool(int)
- 创建可扩容的带缓冲的线程池
(1)使用案例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newCachedThreadPool();//一个池n个线程
//模拟10个用户来办理业务,每个用户就是来自于外部的请求线程
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
//TimeUnit.MICROSECONDS.sleep(200);
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
当把该句不注释运行:
当把该句注释运行:
- 可以看到这种创建方式会自动根据执行任务来分配不同数量的线程
(2)源码及特点
主要特点:
- 1.创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
- 2.创建的线程池的corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60s,就销毁线程
以下3种待补充
2.4 Excutors.newScheduledThreadPool(int)
2.5 Excutors.newSingleThreadScheduledExecutor(int)
2.6 Excutors.newWorkStealingPool(int)
- java8新增,使用目前机器上可用的处理器作为它的并行级别
3.创建ThreadPoolExecutor的7大参数
通过上述前三种的源码可以发现
- 它们都是创建ThreadPoolExecutor对象,并且它们都传入了5个参数
实际上在创建ThreadPoolExecutor对象的时候,可以向构造方法种传入7个参数,用于初始化线程池的一些参数,源码如下:
- 1.corePoolSize:线程池中的常驻核心线程数
- 在创建线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务
- 当线程池中的线程数达到corePoolSize后,就会把到达的任务放到缓存队列当中
- 2.maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- 当一个新任务被提交到池中,如果当前运行线程小于核心线程数(corePoolSize),即使当前有空闲线程,也会新建一个线程来处理新提交的任务;如果当前运行线程数大于核心线程数(corePoolSize)并小于最大线程数(maximumPoolSize),只有当等待队列已满的情况下才会新建线程。
- 3.keeppAliveTime:多余的空闲线程的存活时间
- 当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTIme值时,多余空闲线程会被销毁直到剩下corePoolSize个线程为止
- 默认情况下当线程池中的线程数大于corePoolSize时keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
- 4.unit:keeppAliveTime的时间单位
- 5.workQueue:任务队列,被提交但尚未被执行的任务
- 6.threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
- 7.handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝请求执行的runnable的策略
我们使用银行场景来理解以上参数:
- 营业厅(线程池)刚开始有两个当值窗口(核心线程)在办理业务(处理任务)
- 当办理业务的人比当值窗口少的时候,那么当值窗口就够了
- 当办理业务的人多于当值窗口的时候,多余的乘客去候客区(阻塞队列)等着
- 但是候客区的座位是有限的(阻塞队列是有界的),代表人已经非常多了,就开启加班窗口,开启到银行窗口最大值(最大线程数)
- 当银行窗口开到了最大,候客区也坐满了,还有人来,保安就对来的人进行拒绝,告诉今天暂时办理的人太多了,其他时间再来
- 当过了高峰期,人减少了,加班窗口长时间(keepAliveTime)没有被使用时(除过核心线程外新增的线程变空闲),就关闭几个加班窗口(空闲线程)
4.线程池的底层工作原理
原理图:
流程图:
- 1.在创建了线程池后,等待提交过来的任务请求
- 2.当调用execute方法添加一个请求任务时,线程池会做如下判断:
- 2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
- 2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
- 2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
- 3.当一个线程完成任务时,它会从队列中取下一个任务来执行
- 4.当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
- 所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
5.线程池的4大拒绝策略
什么是拒绝策略
- 上面已经做了解释,等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务,这时就需要拒绝策略机制合理的处理这个问题
jdk的4种拒绝策略:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果允许任务丢失,这是一种最好的方案
- 可以发现以上内置拒绝策略均实现了RejectedExecutionHandler接口
6.实际工作中使用哪一个线程池?——自定义线程池
阿里巴巴开发手册中描述如下:
6.1 自定义线程池(即自己传入参数来new ThreadPoolExcutor)使用案例
import java.util.concurrent.*;
public class MyThreadPoolDemo {
public static void main(String[] args) {
//自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//模拟5个用户来办理业务,每个用户就是来自于外部的请求线程
try {
for (int i = 0; i < 5; i++) {
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
}
- 5个线程,核心线程和阻塞队列刚好能容纳(2+3),所以仅仅两个核心线程在执行任务
当把上述代码的for循环中的5增加到8
- 可以看到启用了核心线程以外的线程直到最大线程数5来执行任务,此时线程的数量并未超过max+阻塞队列容量
当把上述代码的for循环中的8增加到9
- 超过max+阻塞队列容量,启动拒绝策略,由于使用了AbortPolicy,抛出java.util.concurrent.RejectedExecutionException异常
以下是演示另外3种拒绝策略:将任务数增加到10
将上述代码中的拒绝策略替换为,运行
- main线程进行的调用,所以当任务超过max+阻塞队列容量,将多余的任务会交回给它的调用者线程进行处理
将上述代码中的拒绝策略替换为,运行
- 只有8个任务被执行,等待最久的两个被抛弃
将上述代码中的拒绝策略替换为,运行
- 多余的任务直接被丢弃
6.2 如何合理配置的线程数?
既然在使用中都是我们自己传入该7大参数,那么如何合理配置的线程数?
我们应该判断,我们的业务是CPU密集型还是IO密集型,来进行不同的配置:
(1)CPU密集型
我们可以通过如下的代码查看我们的CPU的核数,我的是8核
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量
一般公式:CPU核数+1个线程的线程池
(2)IO密集型
方法1:
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
方法2:
IO密集型,即该任务需要大量的IO,即大量的阻塞
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待上
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间
IO密集型时,大部分被阻塞,故需要多配置线程数
- 参考公式:CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
- 比如8核CPU:8/(1-0.9)=80个线程数
当然以上都是经验值,最好的方式还是根据实际情况测试得出最佳配置。