线程池
为什么要使用线程池
在不使用线程池的情况下,我们使用多线程执行任务会存在一些弊端:
- 难以管理线程生命周期:线程的创建、启动、暂停终止等操作,容易出现并发问题,代码复杂度也会增加,导致手动管理线程生命周期很困难
- 难以控制并发数:手动控制并发执行任务的数量,在执行的任务过多时,容易造成系统资源不足或者性能下降,任务太少,又无法充分利用资源
- 难以控制系统资源消耗:每次创建线程都需要分配管理系统资源,包括内存、线程栈等。在频繁的创建和销毁线程,会增加系统的资源消耗,降低系统的性能,并可能导致资源耗尽的风险
当我们使用线程池就可以有效地解决这些问题,通过维护合理数量的线程来处理并发任务,可以避免频繁创建和销毁线程,减少上下文切换和任务调度开销,而且线程池提供了对线程的管理和监控机制,能根据任务的到达情况动态分配和管理这些线程,从而有效地管理线程的生命周期,提高系统资源的利用率。
线程池的概念
线程池是一种使用池化技术管理和复用线程的并发编程机制,它将多个线程预先存储在一个 “ 池子 ” 内,这些线程可以被重复使用来执行多个任务。从而避免频繁创建和销毁线程所带来的开销。 线程池的核心组成部分:
- 任务队列(taskQueue):用于等待执行的任务,线程池中的线程空闲时,会从队列中取出任务进行执行
- 任务接口(task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等
- 线程池管理器(threadPool):负责创建和管理线程池,包括负责创建、启动、暂停和销毁线程,在线程池初始化时会创建一定数量的线程,并在需要时动态地调整线程数量
- 工作线程(poolWorker):线程池中的实际执行者,在没有任务时处于等待状态,可以循环地执行任务
线程池架构说明
java中线程池是通过Executor框架实现的,该框架中用到了Executor,Executors(代表工具类),ExecutorService,ThreadPoolExecutor这几个类
线程池的使用
创建线程池
使用 Executors 工具类创建线程池
-
Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池
- 执行长期的任务,性能好很多
- 创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待
-
Executors.newSingleThreadExecutor:创建一个只有1个线程的 单线程池
- 一个任务一个任务执行的场景
- 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
-
Executors.newCacheThreadPool(); 创建一个可扩容的线程池
- 执行很多短期异步的小程序或者负载教轻的服务器
- 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
-
Executors.newScheduledThreadPool(int corePoolSize):线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
使用 ThreadPoolExecutor 类创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60L, // 线程空闲超过60秒则销毁
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>() // 任务队列
);
使用案例:模拟20个用户来办理业务,用5个线程处理20个任务请求
示例代码如下:
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
//也可以使用 ThreadPoolExecutor 创建线程池
//ThreadPoolExecutor executorService = new ThreadPoolExecutor(
// 5, // 核心线程数
// 10, // 最大线程数
// 60L, // 线程空闲超过60秒则销毁
// TimeUnit.SECONDS,
// new LinkedBlockingQueue<Runnable>() // 任务队列
//);
try{
for(int i=1;i<=20;i++){
final int tmpInt =i;
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号办理了业务");
});
}
}finally {
executorService.shutdown();
}
}
执行结果:共有5个线程,办理了20个客户的业务
pool-1-thread-1 线程给1号客户办理了业务
pool-1-thread-4 线程给4号客户办理了业务
pool-1-thread-3 线程给3号客户办理了业务
pool-1-thread-2 线程给2号客户办理了业务
pool-1-thread-3 线程给8号客户办理了业务
pool-1-thread-4 线程给7号客户办理了业务
pool-1-thread-5 线程给5号客户办理了业务
pool-1-thread-1 线程给6号客户办理了业务
pool-1-thread-5 线程给12号客户办理了业务
pool-1-thread-4 线程给11号客户办理了业务
pool-1-thread-3 线程给10号客户办理了业务
pool-1-thread-2 线程给9号客户办理了业务
pool-1-thread-3 线程给16号客户办理了业务
pool-1-thread-4 线程给15号客户办理了业务
pool-1-thread-5 线程给14号客户办理了业务
pool-1-thread-1 线程给13号客户办理了业务
pool-1-thread-5 线程给20号客户办理了业务
pool-1-thread-4 线程给19号客户办理了业务
pool-1-thread-3 线程给18号客户办理了业务
pool-1-thread-2 线程给17号客户办理了业务
创建周期性执行任务的线程池
线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为Integer.MAX_VALUE 的线程池
Executors.newScheduledThreadPool(int corePoolSize)
其底层使用的是ScheduledThreadPoolExecutor实现,ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
执行方法
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* delay:延时执行任务的时间
* unit:延迟时间单位
*/
public ScheduledFuture<?> schedule(Runnable command,
long delay,
TimeUnit unit)
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* initialDelay 第一次执行任务延迟时间
* period 连续执行任务之间的周期,从上一个任务开始执行时计算延迟多少开始执行下一个任务,但是还会等上一个任务结束之后。
* unit:延迟时间单位
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit)
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* command:执行的任务 Callable或Runnable接口实现类
* initialDelay 第一次执行任务延迟时间
* delay:连续执行任务之间的周期,从上一个任务全部执行完成时计算延迟多少开始执行下一个任务
* unit:延迟时间单位
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit)
线程池 7 大参数
-
corePoolSize:核心线程数(线程池中常驻的线程)
- 在创建线程池后,当有请求任务来时,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的队列放到缓存队列中
-
maximumPoolSize:能容纳的最大线程数,此值必须
大于等于 1
- 当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池 的拒绝策略了
-
keepAliveTime:空闲线程存活时间
- 当线程池的数量超过 corePoolSize ,且空闲时间达到了 keepAliveTime,此时,空闲线程就会被销毁,只留下
核心线程
运行
- 当线程池的数量超过 corePoolSize ,且空闲时间达到了 keepAliveTime,此时,空闲线程就会被销毁,只留下
-
unit:keepAliveTime的单位(空闲线程存活时间单位)
- TimeUnit.NANOSECONDS;纳秒
- TimeUnit.MICROSECONDS;微秒
- TimeUnit.MILLISECONDS;毫秒
- TimeUnit.SECONDS;秒
- TimeUnit.MINUTES;分钟
- TimeUnit. HOURS;小时
- TimeUnit.DAYS;天
-
workQueue:任务队列,存放被提交还未执行的任务
-
ThreadFactory:创建线程的工厂类
-
handler:拒绝策略,当任务队列满了之后,且工作线程数大于最大线程数时,就会触发线程池的拒绝策略
拒绝策略
- CallerRunsPolicy:当线程池已经达到最大线程数,且任务队列已经满了,新提交的任务将会被提交者所在的线程执行。这种策略可以确保新提交的任务一定会被执行,但是如果提交任务的线程也处于高负载的状态时,可能会导致性能下降
- AbortPolicy:线程池默认的拒绝策略,当线程池已经达到最大线程数,并且任务队列已经满了,新提交的任务将被立即拒绝并抛出RejectedExecutionException异常
- DiscardPolicy:当线程池已经达到最大线程数,并且工作队列已经满了,新提交的任务将会被直接丢弃,且不会抛出任何异常
- DiscardOldestPolicy:当线程池达到最大线程数,且任务队列已经满了,新提交的任务将会替换带队列中最早的任务。这种策略可以避免新提交的任务直接被丢弃,但是替换掉最早的任务可能导致某些任务无法执行
除了上述4种拒绝策略外,还可以通过实现 RejectedExecutionHandler 接口,重写 rejectedExecution() 方法 自定义拒绝策略
线程池的运行流程
流程描述:
-
在创建了线程池后,等待提交过来的任务请求
-
当调用execute()方法添加一个请求任务时,线程池会做出如下判断
- 如果正在运行的线程数量小于corePoolSize,则立即创建线程运行这个任务
- 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
- 如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
- 如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做,空闲超过一定时间(keepAliveTime)时,线程池会判断:
- 如果当前运行的线程数量大于 corePoolSize ,那么这个线程就会被停掉
- 线程池的所有任务完成后,最终都会缩到 corePoolSize 的大小
自定义线程池
创建一个核心线程为2,最大线程数为5,队列容量为3 的线程池。
使用默认拒绝策略(new ThreadPoolExecutor.AbortPolicy())
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
1,//空闲线程存活时间
TimeUnit.SECONDS,//存活时间单位
new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
Executors.defaultThreadFactory(),//默认 ThreadFactory
new ThreadPoolExecutor.AbortPolicy()//默认拒绝策略
);
try{
for(int i=1;i<=9;i++){
final int tmpInt =i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
});
}
}finally {
threadPoolExecutor.shutdown();
}
}
执行结果:在执行到第9个任务的时候,触发了拒绝策略
pool-1-thread-1 线程给1号客户办理了业务
pool-1-thread-2 线程给2号客户办理了业务
pool-1-thread-4 线程给7号客户办理了业务
pool-1-thread-3 线程给6号客户办理了业务
pool-1-thread-1 线程给4号客户办理了业务
pool-1-thread-2 线程给3号客户办理了业务
pool-1-thread-4 线程给5号客户办理了业务
pool-1-thread-5 线程给8号客户办理了业务
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.avgrado.demo.thread.ThreadPoolDemo$$Lambda$1/932172204@76fb509a rejected from java.util.concurrent.ThreadPoolExecutor@300ffa5d[Running, pool size = 5, active threads = 1, queued tasks = 0, completed tasks = 7]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2047)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:823)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1369)
at com.avgrado.demo.thread.ThreadPoolDemo.main(ThreadPoolDemo.java:33)
我们设置的拒绝策略是默认的AbortPolicy,触发时会抛异常
触发条件是,请求的线程大于 阻塞队列大小 + 最大线程数 = 8 的时候,也就是说第9个线程来获取线程池中的线程时,就会触发拒绝策略,抛出异常导致程序退出
使用回退拒绝策略(new ThreadPoolExecutor.CallerRunsPolicy())
CallerRunsPolicy拒绝策略,也称为回退策略,触发时会把新提交的任务给提交者所在的线程执行
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
1,//空闲线程存活时间
TimeUnit.SECONDS,//存活时间单位
new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
Executors.defaultThreadFactory(),//默认 ThreadFactory
new ThreadPoolExecutor.CallerRunsPolicy()//CallerRunsPolicy拒绝策略
);
try{
for(int i=1;i<=9;i++){
final int tmpInt =i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
});
}
}finally {
threadPoolExecutor.shutdown();
}
}
执行结果:最终是main线程执行了第9个任务
pool-1-thread-1 线程给1号客户办理了业务
pool-1-thread-3 线程给6号客户办理了业务
pool-1-thread-1 线程给3号客户办理了业务
pool-1-thread-4 线程给7号客户办理了业务
main 线程给9号客户办理了业务
pool-1-thread-2 线程给2号客户办理了业务
pool-1-thread-5 线程给8号客户办理了业务
pool-1-thread-1 线程给5号客户办理了业务
pool-1-thread-3 线程给4号客户办理了业务
使用Discard 拒绝策略(new ThreadPoolExecutor.Discard())
Discard 拒绝策略触发时,新提交的任务将会被直接丢弃,且不会抛出任何异常
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
1,//空闲线程存活时间
TimeUnit.SECONDS,//存活时间单位
new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
Executors.defaultThreadFactory(),//默认 ThreadFactory
new ThreadPoolExecutor.Discard()//Discard 拒绝策略
);
try{
for(int i=1;i<=9;i++){
final int tmpInt =i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
});
}
}finally {
threadPoolExecutor.shutdown();
}
}
执行结果:并不抛出异常
pool-1-thread-1 线程给1号客户办理了业务
pool-1-thread-3 线程给6号客户办理了业务
pool-1-thread-1 线程给3号客户办理了业务
pool-1-thread-2 线程给2号客户办理了业务
pool-1-thread-1 线程给5号客户办理了业务
pool-1-thread-3 线程给4号客户办理了业务
pool-1-thread-5 线程给8号客户办理了业务
pool-1-thread-4 线程给7号客户办理了业务
使用 DiscardOldestPolicy拒绝策略(new ThreadPoolExecutor.DiscardOldestPolicy())
DiscardOldestPolicy 拒绝策略触发时,新提交的任务将会替换带队列中最早的任务,且不会抛出任何异常
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,//核心线程数
5,//最大线程数
1,//空闲线程存活时间
TimeUnit.SECONDS,//存活时间单位
new ArrayBlockingQueue<>(3),//任务队列,指定容量为3
Executors.defaultThreadFactory(),//默认 ThreadFactory
new ThreadPoolExecutor.DiscardOldestPolicy()//DiscardOldestPolicy拒绝策略
);
try{
for(int i=1;i<=9;i++){
final int tmpInt =i;
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 线程给"+String.valueOf(tmpInt)+"号客户办理了业务");
});
}
}finally {
threadPoolExecutor.shutdown();
}
}
执行结果:可以看出3号客户的任务被直接丢弃掉了
pool-1-thread-1 线程给1号客户办理了业务
pool-1-thread-3 线程给6号客户办理了业务
pool-1-thread-2 线程给2号客户办理了业务
pool-1-thread-5 线程给8号客户办理了业务
pool-1-thread-3 线程给5号客户办理了业务
pool-1-thread-1 线程给4号客户办理了业务
pool-1-thread-4 线程给7号客户办理了业务
pool-1-thread-2 线程给9号客户办理了业务
线程池的参数配置
如何在生产环境中配置 corePoolSize 和 maximumPoolSize ?
根据具体的业务来配置,分为
- CPU 密集型:
- CPU密集的意思是该任务需要大量运算而没有阻塞,CPU一直全速运行
- CPU 密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),在单核CPU上,无论开几个模拟的多线程,该任务都不可能得到加锁,因为CPU的总算力是固定的
- CPU 密集型任务配置尽可能少的配置线程数量:线程数量 = CPU核数+1个线程数
- I O 密集型
- I O 密集型不是在一直执行任务,应该多配置线程数量
- IO密集型,即该任务需要大量的IO操作,即大量的阻塞
- 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上
- 所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
- IO密集时,大部分线程都被阻塞,故需要多配置线程数:线程数量=CPU核数 / (1 - 阻塞系数) 阻塞系数为
0.8~0.9
,比如8核CPU ,阻塞系数取0.9 ,设置线程数量 = 8/(1-0.9) = 80