什么是线程池的拒绝策略,当拒绝策略生效后,会导致数据丢失吗?

1. 什么是线程池的拒绝策略

线程池的拒绝策略(Rejection Policy)是当线程池无法接受新的任务时所采取的策略。通常,线程池会在以下两种情况下拒绝任务:

  • 线程池已满:线程池中的所有工作线程都在忙碌,且无法创建新的线程(根据 corePoolSizemaximumPoolSize 的配置)。
  • 任务队列已满:用于存放等待执行任务的队列已达到其容量上限,不能再容纳新的任务。

当线程池达到这些限制时,无法再接收新的任务。这时,线程池会根据预定义的拒绝策略来处理这些被拒绝的任务。

2. 线程池的拒绝策略类型

Java 中的 ThreadPoolExecutor 提供了几种内置的拒绝策略,位于 java.util.concurrent 包中的 RejectedExecutionHandler 接口下。常见的拒绝策略有以下四种:

1. AbortPolicy (默认策略)

当线程池拒绝任务时,直接抛出 RejectedExecutionException,停止任务的提交。这是最常用的拒绝策略,因为它能够立即通知开发者有任务被拒绝,便于发现问题。

new ThreadPoolExecutor.AbortPolicy();

特点

  • 抛出异常,通知调用方任务被拒绝。
  • 适用于希望程序能够快速发现线程池过载问题的场景。

2. CallerRunsPolicy

调用线程(提交任务的线程)直接执行被拒绝的任务,而不再将任务提交到线程池。这种策略可以有效地降低任务的提交速度,间接减轻线程池的压力。

new ThreadPoolExecutor.CallerRunsPolicy();

特点

  • 任务不会被丢弃,而是由调用线程执行任务。
  • 减少任务提交的速度,防止线程池继续超载。
  • 适用于希望确保任务执行,但系统压力不能过大的场景。

3. DiscardPolicy

直接丢弃被拒绝的任务,不抛出任何异常,也不做任何处理。这种策略比较危险,因为它可能导致任务丢失,且无法知道任务何时被丢弃。

new ThreadPoolExecutor.DiscardPolicy();

特点

  • 任务被丢弃,不执行,也不抛异常。
  • 适用于不关心任务丢失、且不希望影响其他任务执行的场景。

4. DiscardOldestPolicy

丢弃任务队列中最早的任务,然后尝试重新提交新的任务。适合希望优先处理新任务的场景。

new ThreadPoolExecutor.DiscardOldestPolicy();

特点

  • 丢弃等待队列中最老的任务,腾出空间以处理新的任务。
  • 适用于任务之间没有严格顺序要求的场景。

3. 自定义拒绝策略

除了内置的拒绝策略,你还可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略。例如,你可以选择将被拒绝的任务记录到日志或将其放入另一个任务队列进行后续处理。

class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义拒绝策略,如记录日志或发送告警
        System.out.println("Task " + r.toString() + " rejected");
    }
}

4. 拒绝策略生效后会导致数据丢失吗?

拒绝策略是否会导致数据丢失取决于所选择的策略和任务的实现方式:

  • AbortPolicy:抛出异常,任务没有执行,任务实际上是被拒绝的。这种策略通常不会导致数据丢失,因为可以通过捕获异常进行错误处理。

  • CallerRunsPolicy:任务不会丢失,只是由提交任务的线程来执行。如果调用线程有能力处理这个任务,那么不会导致数据丢失。但如果调用线程因其他原因过载,任务可能会执行得很慢。

  • DiscardPolicy:任务被直接丢弃,可能导致数据丢失。因为没有任何异常抛出,任务也不会执行,无法恢复。

  • DiscardOldestPolicy:最早的任务被丢弃,以便新的任务进入队列。由于最早的任务没有执行,因此可能导致数据丢失。

5. 如何防止数据丢失

如果你想避免数据丢失,可以采取以下几种措施:

  • 选择合适的拒绝策略:例如,CallerRunsPolicy 能确保任务最终被执行,即使线程池超载,也不会丢失任务。

  • 日志记录和告警:在自定义拒绝策略中,可以记录被拒绝任务的日志或发送告警,便于你手动处理这些被拒绝的任务。

  • 任务持久化:将任务放入持久化存储(如数据库或消息队列),确保任务即使被拒绝也不会丢失,可以在稍后重新执行。

  • 调整线程池配置:通过合理配置 corePoolSizemaximumPoolSize 和队列容量,可以避免线程池过载。也可以动态调整这些参数以适应负载变化。

  • 降级服务:在超载情况下,可以引入降级机制,如限制某些低优先级任务的执行,确保高优先级任务得到执行。

6. 总结

  • 线程池拒绝策略 决定了当线程池无法接受新任务时,如何处理被拒绝的任务。常见的策略包括 AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy
  • 拒绝策略是否会导致数据丢失 取决于具体策略:
    • AbortPolicy 抛出异常,任务没有执行;
    • CallerRunsPolicy 由调用线程执行,不会丢失任务;
    • DiscardPolicyDiscardOldestPolicy 都可能导致任务丢失。
  • 防止数据丢失 可以通过调整线程池配置、选择合适的拒绝策略、自定义拒绝处理逻辑或将任务持久化等方式来实现。

在 Java 中,要将线程池的拒绝策略设置为 CallerRunsPolicy,可以在创建 ThreadPoolExecutor 实例时,指定该策略。CallerRunsPolicy 会使得当线程池和任务队列都满时,由提交任务的线程(即调用线程)来执行被拒绝的任务。

设置拒绝策略为 CallerRunsPolicy 的步骤:

  1. 创建线程池时使用 ThreadPoolExecutor 构造函数。
  2. 指定 RejectedExecutionHandlernew ThreadPoolExecutor.CallerRunsPolicy()

示例代码:

import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个核心线程数为2,最大线程数为4,队列容量为2的线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数
                4, // 最大线程数
                60L, // 线程空闲时间
                TimeUnit.SECONDS, // 时间单位
                new LinkedBlockingQueue<>(2), // 任务队列容量为2
                new ThreadPoolExecutor.CallerRunsPolicy() // 设置拒绝策略为 CallerRunsPolicy
        );

        // 提交多个任务
        for (int i = 0; i < 10; i++) {
            executor.submit(() -> {
                System.out.println(Thread.currentThread().getName() + " is executing task");
                try {
                    Thread.sleep(1000); // 模拟任务执行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        executor.shutdown();
    }
}

解释:

  • 核心线程数 (corePoolSize):这里设置为 2,表示线程池在空闲时至少保留 2 个线程。
  • 最大线程数 (maximumPoolSize):设置为 4,表示线程池在高负载时最多可以扩展到 4 个线程。
  • 任务队列 (LinkedBlockingQueue):设置队列容量为 2,表示线程池最多可以在队列中缓存 2 个任务。
  • 拒绝策略 (CallerRunsPolicy):当线程池和任务队列都满时,新任务不会被丢弃,而是由提交任务的线程执行。

运行过程说明:

  1. 前 4 个任务:线程池会启动最多 4 个线程处理这 4 个任务(2 个核心线程+2 个额外线程)。
  2. 任务队列中的 2 个任务:接下来到来的 2 个任务会被放入队列,等待执行。
  3. 第 7 个及以后的任务:当线程池中的 4 个线程忙碌,且队列中的 2 个任务也已经排满,CallerRunsPolicy 策略会将第 7 个及后续任务交给调用线程(即 main 线程)来执行。这可以减缓线程池的负载,防止任务被丢弃。

总结:

  • 设置 CallerRunsPolicy 策略可以确保线程池饱和时,任务不会丢失,而是由调用线程来执行。
  • 这种策略有助于防止过载,同时减缓任务提交的速度。

ThreadPoolTaskExecuter, 那么如何设置拒绝策略为 CallerRunsPolicy?

ThreadPoolTaskExecutor 是 Spring 中对 Java 并发工具 ThreadPoolExecutor 的封装。它提供了更便捷的配置方式,广泛用于处理并发任务。要在 ThreadPoolTaskExecutor 中设置拒绝策略为 CallerRunsPolicy,可以通过设置其 setRejectedExecutionHandler 方法来实现。

设置步骤

  1. 创建并配置 ThreadPoolTaskExecutor:使用 setRejectedExecutionHandler 方法设置拒绝策略为 CallerRunsPolicy
  2. 在 Spring 环境中配置:可以通过 Java 配置类或 XML 配置来设置。

1. Java 配置方式

通过 Java 配置类设置 CallerRunsPolicy

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

@Configuration
public class TaskExecutorConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 核心线程数,线程池维护线程的最小数量
        executor.setCorePoolSize(2);
        // 最大线程数,线程池维护线程的最大数量
        executor.setMaxPoolSize(4);
        // 队列容量
        executor.setQueueCapacity(2);
        // 线程空闲时间
        executor.setKeepAliveSeconds(60);
        // 拒绝策略设置为 CallerRunsPolicy
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

        // 初始化
        executor.initialize();

        return executor;
    }
}

2. Spring Boot 应用中的配置

如果你使用的是 Spring Boot,可以通过配置类注入 ThreadPoolTaskExecutor。以上的配置方式可以直接用于 Spring Boot 应用。

3. XML 配置方式

如果你使用 XML 来配置 ThreadPoolTaskExecutor,可以通过以下方式来设置拒绝策略:

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <!-- 核心线程数 -->
    <property name="corePoolSize" value="2" />
    <!-- 最大线程数 -->
    <property name="maxPoolSize" value="4" />
    <!-- 队列容量 -->
    <property name="queueCapacity" value="2" />
    <!-- 线程空闲时间 -->
    <property name="keepAliveSeconds" value="60" />
    <!-- 设置拒绝策略为 CallerRunsPolicy -->
    <property name="rejectedExecutionHandler">
        <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
    </property>
</bean>

解释:

  • corePoolSize:核心线程数,表示线程池中保留的最少线程数。
  • maxPoolSize:最大线程数,表示线程池能够容纳的最大线程数。
  • queueCapacity:任务队列的容量,超过这个容量后将触发拒绝策略。
  • keepAliveSeconds:线程空闲时间,当线程池中的线程数超过核心线程数且这些线程处于空闲状态时,线程会被回收。
  • rejectedExecutionHandler:设置拒绝策略为 CallerRunsPolicy,即当线程池和队列都满时,由调用线程执行任务。

4. 总结

  • ThreadPoolTaskExecutor 是 Spring 对 ThreadPoolExecutor 的封装,通过设置 setRejectedExecutionHandler 方法可以指定拒绝策略。
  • CallerRunsPolicy 确保任务在队列满或线程池满时,由调用线程(即提交任务的线程)执行,而不会丢弃任务。
  • 你可以在 Java 配置类或 XML 中配置 ThreadPoolTaskExecutor 的拒绝策略,确保任务被处理而不丢失。
posted @ 2024-09-24 23:33  gongchengship  阅读(126)  评论(0)    收藏  举报