目录
1.概述
2.线程池的优势
2.1.线程池为什么使用自定义方式?
2.2.封装的线程池工具类有什么好处?
3.线程池的七大参数
3.线程池的创建
3.1. 固定数量的线程池
3.2. 带缓存的线程池
3.3. 执⾏定时任务
3.4. 定时任务单线程
3.5. 单线程线程池
3.6. 根据当前CPU⽣成线程池
3.7. ThreadPoolExecutor★★★
4.使用线程池的最佳实践
4.1.拥有适当数量的线程
4.2.使用合适的工作队列
4.3.处理异常
4.4.使用线程池内置的监控和调试工具
5.线程池的使用案例
5.1.引入jar包
5.2.初始化线程池
5.3.测试案例
5.结论
6.鸣谢
1.概述
线程池是一种常见的多线程编程技术,它允许我们在系统中使用一个固定数量的线程来执行任务,以免过多的线程拉低了系统的性能。在本文中,我们将探讨线程池的使用和一些最佳实践,以便在您的代码中获得更好的性能和可维护性。
线程池是一种用于管理和调度多个线程的技术。线程池主要由三个部分组成:
- 线程管理器:负责启动、停止和管理线程池中的线程。
- 工作队列:用于存储要执行的任务。
- 线程池:包含线程管理器和工作队列。
线程池的工作原理如下:
1)当需要执行一个任务时,线程池会从工作队列中获取一个任务。
2)线程管理器会从线程池中获取一个可用的线程来执行任务。
3)任务执行完成后,线程会返回到线程池中,等待下一个任务的分配。
2.线程池的优势
2.1.线程池为什么使用自定义方式?
因为 java 自带线程池都会有可能造成内存不足的问题。自定义线程池,根据服务器配置定制线程池核心线程、最大线程等,是最好的方式。
2.2.封装的线程池工具类有什么好处?
- 扩展性高
- 可注解形式实现执行
- 可根据业务需要注册不同的线程池,区分业务模块使用
- 可以执行无返回值线程任务,可以执行有返回值的线程任务
3.线程池的七大参数
核心线程数、最大线程数、多余线程存活时间、时间单位、线程工厂、阻塞队列、拒绝策略
/**
* @param corePoolSize 核心线程数 -> 线程池中保持的线程数量,即使它们是空闲的也不会销毁,
* 除非设置了{@code allowCoreThreadTimeOut}核心线程超时时间
* @param maximumPoolSize 最大线程数 -> 线程池中允许接收的最大线程数量
* 如果设定的数量比系统支持的线程数还要大时,会抛出OOM(OutOfMemoryError)异常
* @param keepAliveTime 最大存活时间 -> 当前线程数大于核心线程数的时候,
* 其他多余的线程接收新任务之前的最大等待时间,超过时间没有新任务就会销毁.
* @param unit {@code keepAliveTime}最大存活时间的单位.eg:TimeUnit.SECONDS
* @param workQueue 工作队列 -> 保存任务直到任务被提交到线程池的线程中执行.
* @param threadFactory 线程工厂 -> 当线程池需要创建线程得时候会从线程工厂获取新的实例.
* (自定义ThreadFactory可以跟踪线程池究竟何时创建了多少线程,也可以自定义线程的名称、
* 组以及优先级等信息,甚至可以任性的将线程设置为守护线程.
* 总之,自定义ThreadFactory可以更加自由的设置线程池中所有线程的状态。)
* @param handler 当线程数量等于最大线程数并且工作队列已满的时候,再有新的任务添加进来就会进入这个handler,
* 可以理解为设置拒绝策略(此处不清楚的可以看一下ThreadPoolExecutor中的execute方法的注释)
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
参数 | 说明 |
corePoolSize | 核心线程数量,线程池维护线程的最少数量 |
maximumPoolSize | 线程池维护线程的最大数量 |
keepAliveTime | 非核心线程的最长空闲时间,超过该时间的空闲线程会被销毁 |
unit | keepAliveTime的单位,有NANOSECONDS(纳秒)、MICROSECONDS(微秒)、MILLISECONDS(毫秒)、SECONDS(秒) |
workQueue | 任务缓冲队列(阻塞队列) |
threadFactory | 线程工厂,用于创建线程,一般用默认的即可 |
handler | 线程池对拒绝任务的拒绝策略 |
示例:创建一个核心线程数为5,最大线程数为10,任务队列容量为100的线程池
ThreadPoolExecutor的执行流程如下:
1、当线程池中新加入一个任务时,先判断核心线程数是否达到最大值,如果为false则创建一个核心线程执行任务,如果为true执行第二步;
2、判断当前任务队列是否已满,如果为false,则将任务加入到队列中等待执行;如果为true,则判断当前线程数是否达到最大线程数;
3、如果当前线程数没有达到最大线程数,则创建临时线程来执行任务,如果达到最大线程数,则执行拒绝策略。
拒绝策略指的是线程池中线程数量达到最大值,任务队列为满时,来了新任务的处理方式
ThreadPoolExecutor提供了四种拒绝策略:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常(默认)
CallerRunsPolicy:由调用线程处理该任务( 常用)
DiscardPolicy:丢弃任务,但是不抛出异常。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
当然也可以自定义拒绝策略,只需要实现RejectedExecutionHandler接口即可
3.线程池的创建
线程池使用规范(阿里巴巴)
- 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 自行创建线程,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题。
- 线程池不允许使用 Executors工厂类去创建(阿里的Java规范不推荐使用类
Executors
的静态方法创建线程池),而是通过new ThreadPoolExecutor 的方式,这样 的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
线程池的创建⽅法总共有 7 种,但总体来说可分为 2 类:
1. 通过 ThreadPoolExecutor 创建的线程池;
2. 通过 Executors 创建的线程池。
线程池的创建⽅式总共包含以下 7 种
(其中 6 种是通过 Executors 创建的, 1 种是通过ThreadPoolExecutor 创建)
1. Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
2. Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
3. Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
4. Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
5. Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
6. Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
7. ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,后⾯会详细讲。
3.1. 固定数量的线程池
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
3.2. 带缓存的线程池
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
3.3. 执⾏定时任务
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
3.4. 定时任务单线程
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
3.5. 单线程线程池
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
为什么不直接用线程?单线程的线程池又什么意义?
1. 复用线程。
2. 单线程的线程池提供了任务队列和拒绝策略(当任务队列满了之后(Integer.MAX_VALUE),新来的任务就会拒绝策略)
3.6. 根据当前CPU⽣成线程池
线程池的使用(7种创建方法)_Youcan.的博客-CSDN博客
3.7. ThreadPoolExecutor★★★
线程池的使用(7种创建方法)
a. ThreadPoolExecutor 参数说明
b. 线程池执⾏流程
4.使用线程池的最佳实践
以下是使用线程池的一些最佳实践:
4.1.拥有适当数量的线程
线程池中的线程数量应该根据应用程序的需求来确定。通常情况下,线程池中的线程数量应该等于处理器数量加一。
4.2.使用合适的工作队列
线程池中的工作队列应该根据应用程序的需求来选择。如果需要执行大量的任务并且希望通过控制队列大小来限制系统的负载,则应该使用有界队列。如果希望系统能够保持高吞吐量并且不想限制队列大小,则应该使用无界队列。
4.3.处理异常
线程池中的任务可能会抛出异常,因此我们应该在代码中处理这些异常。当任务抛出异常时,可以将异常记录到日志文件中或者通过线程池的回调函数进行处理。
4.4.使用线程池内置的监控和调试工具
许多线程池都提供了监控和调试工具,这些工具可以帮助我们诊断线程池中的问题。例如,线程池可以提供有关线程数量、队列大小和任务执行速度的统计信息。
5.线程池的使用案例
5.1.引入jar包
<!-- https://mvnrepository.com/artifact/concurrent/concurrent -->
<dependency>
<groupId>concurrent</groupId>
<artifactId>concurrent</artifactId>
<version>1.3.4</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>
5.2.初始化线程池
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @ClassName: AsyncScheduledTaskConfig
* @author: dyt
* @since: 2023/06/16 下午 4:58
*/
@Component
public class AsyncScheduledTaskConfig {
@Bean
public Executor myAsync() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//最大线程数
executor.setMaxPoolSize(100);
//核心线程数
executor.setCorePoolSize(10);
//任务队列的大小
executor.setQueueCapacity(10);
//线程前缀名
executor.setThreadNamePrefix("dyt-thread-");
//线程存活时间
executor.setKeepAliveSeconds(30);
/**
* 拒绝处理策略
* CallerRunsPolicy():交由调用方线程运行,比如 main 线程。
* AbortPolicy():直接抛出异常。
* DiscardPolicy():直接丢弃。
* DiscardOldestPolicy():丢弃队列中最老的任务。
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
//线程初始化
executor.initialize();
return executor;
}
}
5.3.测试案例
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @ClassName: ScheduleTask
* @author: dyt
* @since: 2023/06/19 下午 3:59
*/
@Component
@EnableAsync
public class ScheduleTask {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Async("myAsync")
@Scheduled(fixedRate = 2000)
public void testScheduleTask() {
try{
Thread.sleep(6000);
System.out.println("SpringBoot的定时任务" + Thread.currentThread().getName() + sdf.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行
补充:
使用@Bean(“beanName”)定义线程池
然后在@Async(“beanName”)中引用指定的线程池
5.结论
线程池是一种重要的多线程编程技术,可以帮助我们提高代码的性能和可维护性。通过了解线程池的最佳实践和使用经验,我们可以更好地使用线程池来构建高效的、稳定的多线程应用程序。
6.鸣谢
[1] https://blog.csdn.net/m0_48273471/article/details/124145012
[2] https://blog.csdn.net/YQQAGH178/article/details/119828128
[3] https://blog.csdn.net/qq_43681755/article/details/111057195
[4] https://blog.csdn.net/weixin_48410604/article/details/119386267
[5] https://blog.csdn.net/qq_42889280/article/details/123995250#t0
[6] https://blog.csdn.net/qq_24983911/article/details/94722569
[7] https://blog.csdn.net/qq_42889280/article/details/123995250#t0
[8] https://blog.csdn.net/weixin_37686415/article/details/112549576