【线程池】优化等待队列和拒绝策略

【线程池】优化等待队列和拒绝策略

【一】问题描述

SpringBoot 项目中原生线程池等待队列存在二选一痛点:

(1)用无界队列(LinkedBlockingQueue):任务无限堆积→OOM 内存溢出

(2)用有界队列(ArrayBlockingQueue):队列满→触发拒绝策略→任务丢失

要实现任务不丢失 + 绝不 OOM,核心改造思路是:【有界内存队列(防 OOM) + 持久化兜底队列(防丢失) + 自定义拒绝策略 + 定时补偿执行】,兼顾内存安全和任务可靠性。

【二】核心原则

(1)内存队列必须有界:严格限制大小,杜绝无限堆积导致 OOM

(2)溢出任务持久化:内存队列满了,任务不落内存,直接落盘(Redis/DB/ 文件)

(3)拒绝策略重写:不抛异常、不丢弃任务,转而存入持久化队列

(4)空闲补偿执行:线程池空闲时,把持久化任务回刷到内存队列执行

(5)任务幂等:补偿执行可能重复,需保证任务幂等性

【三】优化方案

【1】轻量级方案:Redis 持久化 + 自定义队列

适合中小并发、简单业务,基于 SpringBoot+Redis 实现,无需额外部署中间件,完美兼顾 OOM 防护 + 任务不丢失。

(1)定义可序列化任务基类

所有线程池任务实现该接口,保证 Redis 可存储:

java 复制代码
import java.io.Serializable;

/**
 * 可持久化任务基类(必须序列化)
 */
public interface PersistableTask extends Runnable, Serializable {
    // 任务唯一标识(用于去重)
    String getTaskId();
}

(2)自定义「有界内存 + Redis 持久化」等待队列

重写队列逻辑:内存满→存 Redis,内存空→从 Redis 拉取,从根源防 OOM、防丢失:

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 自定义线程池等待队列:有界内存队列 + Redis持久化兜底
 * 核心:内存队列有界防OOM,Redis存溢出任务防丢失
 */
public class PersistableTaskQueue extends AbstractExecutorService {
    // 内存有界队列(核心:限制大小,防OOM,根据服务器内存调,建议100~1000)
    private final BlockingQueue<Runnable> memoryQueue;
    // Redis持久化队列(溢出任务存这里)
    private final RedisTemplate<String, Object> redisTemplate;
    // 持久化队列Key
    private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";
    // 内存队列最大容量
    private static final int MEMORY_QUEUE_SIZE = 500;

    public PersistableTaskQueue(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        // 内存队列:有界ArrayBlockingQueue,杜绝OOM
        this.memoryQueue = new ArrayBlockingQueue<>(MEMORY_QUEUE_SIZE);
    }

    // 任务入队:内存能放下→入内存;放不下→入Redis
    public boolean offer(Runnable task) {
        if (memoryQueue.offer(task)) {
            return true;
        }
        // 内存队列满,溢出任务存入Redis(持久化,不丢失)
        if (task instanceof PersistableTask) {
            redisTemplate.opsForList().leftPush(REDIS_QUEUE_KEY, task);
            return true;
        }
        return false;
    }

    // 任务出队:先拿内存,内存空→从Redis拉取
    public Runnable take() throws InterruptedException {
        Runnable task = memoryQueue.poll();
        if (task == null) {
            // 内存空,从Redis拉取溢出任务
            task = (Runnable) redisTemplate.opsForList().rightPop(REDIS_QUEUE_KEY);
            if (task == null) {
                // 无任务,阻塞等待
                task = memoryQueue.take();
            }
        }
        return task;
    }

    // 省略其他重写方法(直接实现即可)
    @Override
    public void shutdown() {}
    @Override
    public List<Runnable> shutdownNow() {return null;}
    @Override
    public boolean isShutdown() {return false;}
    @Override
    public boolean isTerminated() {return false;}
    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) {return false;}
    @Override
    public void execute(Runnable command) {offer(command);}
}

(3)自定义拒绝策略(永不丢弃任务)

原生拒绝策略会丢任务,重写后溢出任务直接入 Redis 持久化:

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 自定义拒绝策略:任务溢出→存入Redis,不丢失、不抛异常
 */
public class PersistableRejectHandler implements RejectedExecutionHandler {
    private final RedisTemplate<String, Object> redisTemplate;
    private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";

    public PersistableRejectHandler(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 触发拒绝策略=线程池满载,直接把任务存入Redis
        if (r instanceof PersistableTask) {
            redisTemplate.opsForList().leftPush(REDIS_QUEUE_KEY, r);
        }
    }
}

(4)SpringBoot 线程池配置(核心)

配置线程池参数,绑定自定义队列和拒绝策略:

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Configuration
public class ThreadPoolConfig {
    // 核心线程数(IO密集型:CPU核心数*2;CPU密集型:CPU核心数+1)
    private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
    // 最大线程数(不要过大,避免线程切换开销)
    private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 4;
    // 空闲线程存活时间
    private static final long KEEP_ALIVE_TIME = 60L;

    @Bean
    public ThreadPoolExecutor persistableThreadPool(RedisTemplate<String, Object> redisTemplate) {
        // 自定义持久化队列
        PersistableTaskQueue taskQueue = new PersistableTaskQueue(redisTemplate);
        // 自定义拒绝策略
        PersistableRejectHandler rejectHandler = new PersistableRejectHandler(redisTemplate);

        return new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.SECONDS,
                taskQueue, // 绑定自定义队列
                new ThreadPoolExecutor.CallerRunsPolicy(), // 备用拒绝策略
                rejectHandler // 核心拒绝策略
        );
    }
}

(5)定时补偿任务(空闲时执行 Redis 溢出任务)

保证 Redis 中的溢出任务最终被执行,无遗漏:

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 定时补偿:将Redis溢出任务回刷到线程池执行
 */
@Component
public class TaskCompensateScheduler {
    private final ThreadPoolExecutor threadPool;
    private final RedisTemplate<String, Object> redisTemplate;
    private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";

    public TaskCompensateScheduler(ThreadPoolExecutor threadPool, RedisTemplate<String, Object> redisTemplate) {
        this.threadPool = threadPool;
        this.redisTemplate = redisTemplate;
    }

    // 每5秒补偿一次(根据业务调频率)
    @Scheduled(fixedRate = 5000)
    public void compensateTask() {
        // 仅当线程池空闲时,拉取Redis任务执行
        if (threadPool.getQueue().size() < 100) {
            Object task = redisTemplate.opsForList().rightPop(REDIS_QUEUE_KEY);
            if (task != null) {
                threadPool.execute((Runnable) task);
            }
        }
    }
}

(6)使用示例(业务任务)

java 复制代码
/**
 * 业务任务(实现PersistableTask,支持持久化)
 */
public class BizTask implements PersistableTask {
    private String taskId;
    private String bizData;

    public BizTask(String taskId, String bizData) {
        this.taskId = taskId;
        this.bizData = bizData;
    }

    @Override
    public String getTaskId() {
        return taskId;
    }

    @Override
    public void run() {
        // 业务逻辑:保证幂等性
        System.out.println("执行任务:" + bizData);
    }
}

// 业务中使用
@Service
public class BizService {
    @Resource
    private ThreadPoolExecutor threadPool;

    public void submitTask() {
        // 提交任务,无论并发多大,都不会OOM、不会丢失
        threadPool.execute(new BizTask("task_001", "测试业务数据"));
    }
}

【2】高并发方案:MQ 中间件解耦(生产核心业务首选)

适合高并发、核心链路业务,彻底把线程池和任务缓冲解耦,100% 防 OOM、防丢失:

(1)线程池只配置小容量有界队列(如 200),杜绝 OOM

(2)任务直接发送到RabbitMQ/RocketMQ/Kafka(消息持久化,不丢失)

(3)MQ 消费者监听消息,提交到线程池执行

(4)消费失败→MQ 重试 / 死信队列,保证任务最终执行

核心优势

(1)线程池无堆积压力→永不 OOM

(2)MQ 消息持久化→任务绝对不丢失

(3)削峰填谷,适配流量波动

【3】数据库持久化方案

(1)使用场景

对实时性要求不高,例如大数据平台计算T-1数据,先创建待运行的任务实例,然后通过定时任务批量执行任务实例。因为这些任务在初始化创建的时候是先创建到数据库实现持久化的,所以不会出现任务丢失的情况;使用有界队列可以保证不会出现队列OOM的问题

(2)设计思路

1-首先,创建n条任务信息到数据库,每条任务有状态、重试次数、重试次数上限等等关键信息

2-然后,初始化线程池,队列初始化有界队列,拒绝策略选择丢弃任务

ThreadPoolExecutor.DiscardPolicy(丢弃新任务策略): 不处理新任务,直接丢弃掉

ThreadPoolExecutor.DiscardOldestPolicy(丢弃最早未处理): 此策略将丢弃最早的未处理的任务请求。

3-xxljob定时调度任务,先到数据库查询状态为【未完成】或状态为【失败】但重试次数没有超过上限的任务n条,遍历到线程池中处理,如果没有被拒绝,状态重置为【运行中】,如果执行成功就修改状态为【成功】,失败就修改状态为【失败】;如果等待队列满了被丢弃就保持原状态,下次定时任务还会捞取满足条件的非成功的实例

相关推荐
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于Spring Boot的体育场地预约管理系统为例,包含答辩的问题和答案
java·spring boot·后端
青槿吖2 小时前
第二篇:告别XML臃肿配置!Spring注解式IOC/DI保姆级教程,从入门到真香
xml·java·开发语言·数据库·后端·sql·spring
摇滚侠3 小时前
讲一讲 SpringMVC,线程变量 ThreadLocal 的使用
java·spring boot·intellij-idea
kuntli3 小时前
BIO NIO AIO核心区别解析
java
Javatutouhouduan3 小时前
京东内部强推HotSpot VM源码剖析笔记(2026新版)
java·jvm·java虚拟机·校招·java面试·java程序员·互联网大厂
imuliuliang4 小时前
怎么下载安装yarn
java
曹牧4 小时前
在 Eclipse 中配置 Maven 和 Gradle 项目以支持增量打包
java·eclipse·maven
_olone4 小时前
牛客每日一题:显生之宙(Java)
java·开发语言·算法·牛客
Sirens.4 小时前
Java 包装类、泛型与类型擦除
java·开发语言·javac