自定义线程池
什么是线程?
线程(Thread)是程序执行流的最小单元。一个进程可以包含多个线程,每个线程负责执行进程中的一部分任务。多线程可以让程序在同一时刻执行多个任务,提高程序的并发性和响应性。
为什么要使用线程
- 提高程序的响应性:通过使用线程,可以将一些耗时的任务放到后台线程中执行,使得主线程不被阻塞,提高了程序的响应性。例如,在用户点击某个按钮时,主线程可以继续响应用户的输入,而耗时任务则可以在后台线程中执行;
- 提高系统资源利用率:多线程可以充分利用多核处理器的性能,提高系统的资源利用率。每个线程可以在不同的处理器核心上并行执行,加快任务的处理速度;
- 实现异步编程:线程可以用于实现异步编程,使得程序可以在等待某个任务完成的同时继续执行其他任务。这种方式可以提高程序的效率,特别是在处理I/O操作等耗时任务时;
为什么要使用线程池
-
降低线程创建和销毁的开销: 线程的创建和销毁是比较昂贵的操作,会消耗系统的资源。使用线程池可以避免频繁地创建和销毁线程,提高了线程的重用性,降低了系统的开销;
-
提供任务队列: 线程池通常包含一个任务队列,用于存储等待执行的任务。当线程池中的线程都在执行任务时,新的任务会被放入任务队列中等待执行,避免任务丢失;
注意:极端情况下,如果任务队列已满且线程池中的线程也都在执行任务,新的任务将无法被存储在队列中,从而可能导致任务丢失。
-
避免系统崩溃: 通过控制并发线程数量和提供任务队列,线程池可以避免系统因为线程过多导致崩溃或资源耗尽的情况;
线程池任务提交流程
线程池的拒绝策略
- AbortPolicy(默认) :当工作队列已满的情况下并且无法添加新的任务,抛出RejectedExecutionException异常
- CallerRunsPolicy :当没有可用的线程来执行任务时,提交任务的线程(通常是调用
execute()
的那个线程)将会执行该任务 - DiscardOldestPolicy :这个策略会尝试丢弃最早提交到线程池的任务(即队列中最旧的任务),以腾出空间给新提交的任务。如果这样做之后仍然无法添加新任务,则会退化为使用
AbortPolicy
。 - DiscardPolicy: 这个策略简单地丢弃无法处理的任务,不做任何额外的通知。也就是说,任务将被静默地丢弃,不会有任何异常抛出
线程池的5种工作状态
线程池的7个核心参数
- corePoolSize :
- 核心线程数。线程池中保持的最小线程数。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了
allowCoreThreadTimeOut
为true
。
- 核心线程数。线程池中保持的最小线程数。即使这些线程处于空闲状态,它们也不会被销毁,除非设置了
- maximumPoolSize :
- 最大线程数。线程池能够容纳的最大线程数。当活动线程数达到此值后,新的任务将会根据拒绝策略进行处理。
- keepAliveTime :
- 非核心线程的存活时间。当线程池中的线程数大于
corePoolSize
时,多余的空闲线程会等待的时间长度,在此期间内如果没有新的任务到来,多余的线程将被销毁。
- 非核心线程的存活时间。当线程池中的线程数大于
- TimeUnit unit :
keepAliveTime
参数的时间单位。常用的有TimeUnit.SECONDS
、TimeUnit.MINUTES
等。
- workQueue :
- 任务队列。这是一个阻塞队列,用来保存等待执行的任务。常见的队列类型包括
ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
等。
- 任务队列。这是一个阻塞队列,用来保存等待执行的任务。常见的队列类型包括
- threadFactory :
- 线程工厂。用于创建新线程的工厂,可以通过它来设置线程的名字、优先级等属性。
- handler :
- 拒绝策略。当线程池不能接收更多任务时(比如线程池已经达到了最大大小并且任务队列也满了),应该采用何种方式处理新到达的任务。可以选择的策略包括但不限于
AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
和DiscardPolicy
。
- 拒绝策略。当线程池不能接收更多任务时(比如线程池已经达到了最大大小并且任务队列也满了),应该采用何种方式处理新到达的任务。可以选择的策略包括但不限于
配置线程池
java
@Configuration
@EnableAsync
public class TheadPoolConfig {
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 核心线程数
executor.setMaxPoolSize(20); // 最大线程数
executor.setQueueCapacity(100); // 队列容量
executor.setThreadNamePrefix("WeblogThreadPool-"); // 线程名前缀
executor.initialize();
return executor;
}
}
在上述配置中,@EnableAsync
注解用于启用 Spring 的异步特性。ThreadPoolTaskExecutor
用于创建一个线程池,我们可以根据实际的业务场景来调整核心线程数、最大线程数、队列容量等参数。
java
@Component
@Slf4j
public class ReadArticleSubscriber implements ApplicationListener<ReadArticleEvent> {
@Autowired
private ArticleMapper articleMapper;
@Override
@Async("threadPoolTaskExecutor")
public void onApplicationEvent(ReadArticleEvent event) {
// 在这里处理收到的事件,可以是任何逻辑操作
Long articleId = event.getArticleId();
// 获取当前线程名称
String threadName = Thread.currentThread().getName();
log.info("==> threadName: {}", threadName);
log.info("==> 文章阅读事件消费成功,articleId: {}", articleId);
articleMapper.increaseReadNum(articleId);
}
}
需要异步的事件使用的@Async来实现异步操作
自定义线程的好处
默认线程池
Java的java.util.concurrent.Executors
类提供了一些静态工厂方法来创建预定义的线程池。例如:
newFixedThreadPool(int nThreads)
:创建一个固定大小的线程池。newSingleThreadExecutor()
:创建一个单线程化的线程池。newCachedThreadPool()
:创建一个可缓存的线程池。newScheduledThreadPool(int corePoolSize)
:创建一个支持定时及周期性任务执行的线程池。
可能会导致大量的线程创建,默认线程池的拒绝策略可能是简单地抛出异常或者静默失败,这不利于错误的诊断和处理
自定义线程池
自定义线程池允许开发者更精细地控制线程池的行为,包括但不限于以下方面:
- 线程数控制:可以根据应用的具体需求来设定核心线程数、最大线程数和线程存活时间。
- 任务队列:可以选择不同类型的队列来优化任务的排队和调度。
- 线程命名和优先级 :可以使用自定义的
ThreadFactory
来创建带有特定名称前缀和优先级的线程。 - 拒绝策略:可以根据业务逻辑实现特定的拒绝策略,而不是简单的丢弃或抛出异常。
- 资源管理:可以更好地控制资源的使用,避免因线程过多而耗尽系统资源。
好处:
- 更好的性能:通过精确控制线程池的大小和其他参数,可以提高系统的响应时间和吞吐量。
- 增强的健壮性 :通过实现适当的拒绝策略,可以在系统过载时提供更可靠的错误处理。
制资源的使用,避免因线程过多而耗尽系统资源。