什么是线程池:
线程池就是事先将多个线程对象放到一个容器中,当使用的时候就不用 new
线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高的代码执行效率
线程池的作用:
- 节省创建子线程的时间
- 线程复用
- 控制最大并发数
- 管理线程
自定义线程池:
1、yml配置
thread:
executor:
corePoolSize: 16
maxPoolSize: 32
maximumPoolSize: 1000L
2、ThreadProperties类
@Data
@Component
// 通过yml中的thread.executor来给属性赋值
@ConfigurationProperties(prefix = "thread.executor")
public class ThreadProperties {
private int corePoolSize=16;
private int maximumPoolSize=32;
private long keepAliveTime=1000L;
private int workQueueNum=5000;
}
3、在容器中放入线程池对象
@Configuration
public class ThreadConfig {
@Autowired
ThreadProperties threadProperties;
@Bean
public ThreadPoolExecutor threadPoolExecutor(){
return new ThreadPoolExecutor(
threadProperties.getCorePoolSize(), //核心线程数
threadProperties.getMaximumPoolSize(), // 最大线程数
threadProperties.getKeepAliveTime(), // 最大空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(12), //阻塞队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
}
}
线程池参数:
corePoolSize:核心线程数,在ThreadPoolExecutor中有一个与它相关的配置:allowCoreThreadTimeOut(默认为false),当allowCoreThreadTimeOut为false时,核心线程会一直存活,哪怕是一直空闲着。而当allowCoreThreadTimeOut为true时核心线程空闲时间超过keepAliveTime时会被回收。
maximumPoolSize:最大线程数,线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。
keepAliveTime:存活时间,当非核心空闲超过这个时间将被回收,同时空闲核心线程是否回收受allowCoreThreadTimeOut影响。
unit:keepAliveTime的单位。
workQueue:任务队列,常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。
threadFactory:线程工厂,ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。
RejectedExecutionHandler:也是一个接口,只有一个方法,当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution法。
默认是抛出一个运行时异常。
阻塞队列:BlockingQuene
BlockingQueue即阻塞队列,是java.util.concurrent下的一个接口。从阻塞这个词可以看出,在某些情况下对阻塞队列的访问可能会造成阻塞。被阻塞的情况主要有如下两种:
1.当队列满了的时候进行入队列操作
2.当队列空了的时候进行出队列操作
BlockingQueue接口主要有以下7个实现类:
1.ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
2.LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
3.PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
4.DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
5.SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
6.LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。
阻塞队列的方法:
抛出异常
add正常执行返回true,element(不删除)和remove返回阻塞队列中的第一个元素 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full 当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException 当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特定值 插入方法,成功ture失败false 移除方法,成功返回出队列的元素,队列里没有就返回null 检查方法,成功返回队列中的元素,没有返回null
一直阻塞
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。 当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出 当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。 返回一个特定值以告知该操作是否成功(典型的是 true / false)。
测试
- ArrayBlockingQueue
@Test
void test1(){
ArrayBlockingQueue<Integer> intBlockingQuene = new ArrayBlockingQueue<>(5);
intBlockingQuene.add(1);
intBlockingQuene.add(2);
intBlockingQuene.add(3);
intBlockingQuene.add(4);
System.out.println(intBlockingQuene.add(5));
intBlockingQuene.add(6);
}
线程池的大小设置
1.需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
2.每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉及到网络传输以及底层系统资源依赖有关系
如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,假如CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1 ;如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。
总结:
- CPU密集型的最佳线程数量=CPU核数(或者CPU核数+1,加1是为了在其他线程出现故障的时候,这个额外的线程可以用)
- IO密集型最佳线程数量=2*CPU核数(或 核心线程数=CPU核心数/(1-阻塞系数))
线程并不是越多越好,越多,每个cpu处理的线程就越多,上下文切换频繁。
混合型线程池设定最佳线程数目 = ((线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
Ncpu=Runtime.getRuntime().availableProcessors()
拒绝策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务
Executors工具类
可以用它快速创建线程池。一般都是手动建线程池,不用这个工具类来创建。
这几个线程池底层都是new ThreadPoolExecutor
线程池的工作原理:
1、一个任务要加到线程池中,如果正在运行的线程数<核心线程数,那么立马创建一个线程在执行这个任务。
2、如果正在运行的线程数>核心线程数,那么将这个任务放到阻塞队列中
3、如果这个队列满了,并且正在运行的线程数量<最大的线程数,创建一个非非核心线程数来执行这个任务。
4、如果这个队列满了,并且正在运行的线程数量=最大的线程数,启用拒绝策略来处理这个任务。
5、当一个线程完成任务时,它会从队列中取下一个任务来执行。
6、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小(allowCoreThreadTimeOut=false的时候)。
所以线程池的所有任务完成后,它最终会收缩到0(allowCoreThreadTimeOut=true的时候)。