线程池拒绝策略:优雅说不的艺术🚫

60 阅读8分钟

当线程池忙不过来时,如何拒绝新任务?是直接抛异常、让调用者自己执行,还是丢弃任务?选择不同,结果大不同!

一、开场:线程池什么时候会拒绝任务?🤔

触发拒绝的条件

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                      // 核心线程数
    5,                      // 最大线程数
    60L, TimeUnit.SECONDS,  // 空闲存活时间
    new ArrayBlockingQueue<>(10), // 队列容量10
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

拒绝时机:

1. 线程数 < 核心线程数 → 创建新线程 ✅
2. 线程数 = 核心线程数 → 放入队列 ✅
3. 队列已满 + 线程数 < 最大线程数 → 创建新线程 ✅
4. 队列已满 + 线程数 = 最大线程数 → 🚫 拒绝!

可视化:

任务到来
  ↓
核心线程忙吗?
├─ 否 → 创建核心线程执行
└─ 是
    ↓
  队列满吗?
  ├─ 否 → 放入队列
  └─ 是
      ↓
    达到最大线程数?
    ├─ 否 → 创建临时线程
    └─ 是 → 💥 执行拒绝策略!

二、JDK自带的4种拒绝策略🎯

策略1:AbortPolicy(中止策略)⛔ 默认

public static class AbortPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        throw new RejectedExecutionException(
            "Task " + r.toString() + " rejected from " + e.toString()
        );
    }
}

特点:

  • 直接抛出RejectedExecutionException
  • 不执行任务
  • 调用者可以捕获异常处理

适用场景:

  • 任务绝对不能丢失
  • 需要调用者感知拒绝并处理

示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(1),
    new ThreadPoolExecutor.AbortPolicy() // 默认策略
);

try {
    executor.execute(() -> System.out.println("任务1"));
    executor.execute(() -> System.out.println("任务2"));
    executor.execute(() -> System.out.println("任务3")); // 💥 抛异常
} catch (RejectedExecutionException e) {
    System.out.println("任务被拒绝: " + e.getMessage());
    // 处理:记录日志、重试、降级
}

输出:

任务1
任务被拒绝: Task ... rejected from ...

策略2:CallerRunsPolicy(调用者运行策略)🏃

public static class CallerRunsPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            r.run(); // 在调用者线程执行!
        }
    }
}

特点:

  • 不抛异常
  • 调用者线程执行任务
  • 阻塞调用者线程

适用场景:

  • 任务不能丢失
  • 可以容忍调用者线程被阻塞
  • 自然的背压机制(反压)

示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(1),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

System.out.println("主线程: " + Thread.currentThread().getName());

executor.execute(() -> {
    System.out.println("任务1: " + Thread.currentThread().getName());
    sleep(1000);
});

executor.execute(() -> {
    System.out.println("任务2: " + Thread.currentThread().getName());
});

executor.execute(() -> {
    System.out.println("任务3: " + Thread.currentThread().getName()); // 主线程执行!
});

输出:

主线程: main
任务1: pool-1-thread-1
任务3: main  ← 在主线程执行
任务2: pool-1-thread-1

优点:

  • 自动降速(调用者被阻塞,提交速度降低)
  • 任务不丢失

缺点:

  • 调用者线程被占用
  • 可能影响调用者的其他任务

策略3:DiscardPolicy(丢弃策略)🗑️

public static class DiscardPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        // 什么都不做,静默丢弃
    }
}

特点:

  • 不抛异常
  • 直接丢弃任务
  • 调用者不知道任务被拒绝

适用场景:

  • 任务可以丢失
  • 不重要的日志、统计
  • 对实时性要求高,旧数据可以丢弃

示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(1),
    new ThreadPoolExecutor.DiscardPolicy()
);

executor.execute(() -> System.out.println("任务1"));
executor.execute(() -> System.out.println("任务2"));
executor.execute(() -> System.out.println("任务3")); // 静默丢弃

Thread.sleep(2000);
executor.shutdown();

输出:

任务1
任务2
(任务3被丢弃,没有输出)

⚠️ 危险: 任务丢失,调用者不知道,可能导致数据丢失!


策略4:DiscardOldestPolicy(丢弃最老任务策略)🕰️

public static class DiscardOldestPolicy implements RejectedExecutionHandler {
    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        if (!e.isShutdown()) {
            e.getQueue().poll(); // 丢弃队列头部任务
            e.execute(r);        // 重新提交当前任务
        }
    }
}

特点:

  • 丢弃队列中最老的任务
  • 然后重新提交当前任务
  • 不抛异常

适用场景:

  • 新任务优先级更高
  • 旧数据可以丢弃(如实时监控)

示例:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    1, 1, 0L, TimeUnit.MILLISECONDS,
    new ArrayBlockingQueue<>(2),
    new ThreadPoolExecutor.DiscardOldestPolicy()
);

executor.execute(() -> {
    System.out.println("任务1");
    sleep(1000);
});

executor.execute(() -> System.out.println("任务2")); // 进队列
executor.execute(() -> System.out.println("任务3")); // 进队列
executor.execute(() -> System.out.println("任务4")); // 丢弃任务2,任务4进队列

Thread.sleep(2000);
executor.shutdown();

输出:

任务1
任务3
任务4
(任务2被丢弃)

三、四种策略对比表📊

策略任务丢失抛异常阻塞调用者适用场景
AbortPolicy❌ 不丢失✅ 抛异常❌ 不阻塞不能丢任务,需感知拒绝
CallerRunsPolicy❌ 不丢失❌ 不抛✅ 阻塞背压机制,自动降速
DiscardPolicy✅ 丢失❌ 不抛❌ 不阻塞任务可丢失,如日志
DiscardOldestPolicy✅ 丢失旧的❌ 不抛❌ 不阻塞新任务优先级高

四、自定义拒绝策略💡

场景1:记录日志并告警

public class LogAndAlertPolicy implements RejectedExecutionHandler {
    
    private static final Logger log = LoggerFactory.getLogger(LogAndAlertPolicy.class);
    private final AtomicLong rejectedCount = new AtomicLong(0);
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        long count = rejectedCount.incrementAndGet();
        
        // 记录日志
        log.error("任务被拒绝! 任务: {}, 线程池状态: 活跃线程={}, 队列大小={}, 总拒绝数={}",
            r.toString(),
            executor.getActiveCount(),
            executor.getQueue().size(),
            count
        );
        
        // 告警(拒绝次数超过阈值)
        if (count % 100 == 0) {
            sendAlert("线程池拒绝任务数达到: " + count);
        }
        
        // 抛异常通知调用者
        throw new RejectedExecutionException("任务被拒绝,已记录日志");
    }
    
    private void sendAlert(String message) {
        // 发送告警:短信、邮件、钉钉等
        System.out.println("🚨 告警: " + message);
    }
}

场景2:异步降级存储

public class AsyncFallbackPolicy implements RejectedExecutionHandler {
    
    private final BlockingQueue<Runnable> fallbackQueue;
    private final ScheduledExecutorService retryExecutor;
    
    public AsyncFallbackPolicy() {
        this.fallbackQueue = new LinkedBlockingQueue<>(10000);
        this.retryExecutor = Executors.newSingleThreadScheduledExecutor();
        
        // 定时重试
        retryExecutor.scheduleWithFixedDelay(this::retryTasks, 1, 1, TimeUnit.SECONDS);
    }
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 放入降级队列
        boolean added = fallbackQueue.offer(r);
        
        if (!added) {
            // 降级队列也满了,记录并丢弃
            log.error("降级队列已满,任务丢弃: {}", r);
        } else {
            log.warn("任务降级到备用队列: {}", r);
        }
    }
    
    private void retryTasks() {
        // 从降级队列取任务重试
        Runnable task;
        while ((task = fallbackQueue.poll()) != null) {
            try {
                task.run(); // 直接执行或提交到其他线程池
            } catch (Exception e) {
                log.error("降级任务执行失败", e);
            }
        }
    }
}

场景3:持久化到数据库

public class PersistencePolicy implements RejectedExecutionHandler {
    
    private final TaskRepository taskRepository;
    
    public PersistencePolicy(TaskRepository taskRepository) {
        this.taskRepository = taskRepository;
    }
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        if (r instanceof PersistableTask) {
            PersistableTask task = (PersistableTask) r;
            
            // 保存到数据库
            taskRepository.save(task);
            
            log.warn("任务被拒绝,已保存到数据库,taskId: {}", task.getTaskId());
        } else {
            // 无法持久化,抛异常
            throw new RejectedExecutionException("任务被拒绝且无法持久化");
        }
    }
}

// 可持久化的任务
interface PersistableTask extends Runnable {
    String getTaskId();
    String serialize();
}

场景4:动态路由到备用线程池

public class FailoverPolicy implements RejectedExecutionHandler {
    
    private final List<ThreadPoolExecutor> backupPools;
    private final AtomicInteger currentIndex = new AtomicInteger(0);
    
    public FailoverPolicy(List<ThreadPoolExecutor> backupPools) {
        this.backupPools = backupPools;
    }
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 轮询备用线程池
        for (int i = 0; i < backupPools.size(); i++) {
            int index = currentIndex.getAndIncrement() % backupPools.size();
            ThreadPoolExecutor backup = backupPools.get(index);
            
            try {
                backup.execute(r);
                log.info("任务转移到备用线程池-{}", index);
                return;
            } catch (RejectedExecutionException e) {
                // 备用池也满了,尝试下一个
                continue;
            }
        }
        
        // 所有备用池都满了
        log.error("所有线程池都已满,任务被丢弃");
        throw new RejectedExecutionException("所有线程池都已满");
    }
}

五、生产环境如何选择?🏭

决策树

任务能丢失吗?
├─ 不能丢失
│  ├─ 需要感知拒绝?
│  │  ├─ 是 → AbortPolicy + 异常处理 ⭐
│  │  └─ 否 → CallerRunsPolicy(背压) ⭐
│  └─ 有备用方案?
│     └─ 是 → 自定义(降级/持久化/备用池)⭐
└─ 可以丢失
   ├─ 新任务更重要?
   │  └─ 是 → DiscardOldestPolicy
   └─ 直接丢弃 → DiscardPolicy

不同场景的推荐策略

1. Web请求处理

// 推荐:CallerRunsPolicy(背压)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10, 50, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

原因:

  • 请求不能丢失
  • 调用者线程执行,自然降速
  • 防止线程池压力过大

2. 异步日志

// 推荐:DiscardPolicy(丢弃)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2, 4, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(10000),
    new ThreadPoolExecutor.DiscardPolicy()
);

原因:

  • 日志丢失影响不大
  • 不能影响主业务
  • 简单高效

3. 订单处理

// 推荐:自定义(告警+持久化)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    20, 100, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(5000),
    new LogAndAlertPolicy() // 自定义
);

原因:

  • 订单绝对不能丢
  • 需要告警通知
  • 需要监控拒绝次数

4. 实时监控数据

// 推荐:DiscardOldestPolicy(丢弃旧数据)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100),
    new ThreadPoolExecutor.DiscardOldestPolicy()
);

原因:

  • 监控数据只关心最新的
  • 旧数据可以丢弃
  • 实时性要求高

六、生产环境最佳实践✅

1. 合理配置线程池参数

// ❌ 错误:使用Executors创建
ExecutorService executor = Executors.newFixedThreadPool(10);

// ✅ 正确:手动配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    10,                                 // 核心线程数
    50,                                 // 最大线程数
    60L, TimeUnit.SECONDS,              // 空闲存活时间
    new LinkedBlockingQueue<>(1000),    // 有界队列
    new ThreadFactoryBuilder()
        .setNameFormat("业务线程池-%d")
        .build(),
    new CustomRejectedPolicy()          // 自定义策略
);

2. 监控线程池状态

public class ThreadPoolMonitor {
    
    private final ThreadPoolExecutor executor;
    private final AtomicLong totalRejected = new AtomicLong(0);
    
    public void monitor() {
        ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
        
        monitor.scheduleAtFixedRate(() -> {
            int activeCount = executor.getActiveCount();
            int poolSize = executor.getPoolSize();
            int queueSize = executor.getQueue().size();
            long completedTasks = executor.getCompletedTaskCount();
            
            log.info("线程池状态: 活跃线程={}, 线程数={}, 队列={}, 已完成={}, 总拒绝={}",
                activeCount, poolSize, queueSize, completedTasks, totalRejected.get()
            );
            
            // 告警
            if (queueSize > 800) {
                alert("队列堆积严重: " + queueSize);
            }
            
            if (activeCount == poolSize && poolSize == executor.getMaximumPoolSize()) {
                alert("线程池已满负荷运行");
            }
            
        }, 10, 10, TimeUnit.SECONDS);
    }
}

3. 优雅关闭

public void shutdown() {
    log.info("开始关闭线程池...");
    
    // 1. 停止接受新任务
    executor.shutdown();
    
    try {
        // 2. 等待60秒
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            // 3. 强制关闭
            log.warn("等待超时,强制关闭");
            List<Runnable> droppedTasks = executor.shutdownNow();
            log.warn("丢弃任务数: {}", droppedTasks.size());
            
            // 4. 再等30秒
            if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
                log.error("线程池无法关闭");
            }
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }
    
    log.info("线程池已关闭");
}

4. 根据场景动态调整

public class DynamicThreadPool {
    
    private final ThreadPoolExecutor executor;
    
    // 根据负载动态调整
    public void adjustPoolSize() {
        int queueSize = executor.getQueue().size();
        int corePoolSize = executor.getCorePoolSize();
        
        if (queueSize > 500 && corePoolSize < 50) {
            // 队列堆积,增加核心线程
            executor.setCorePoolSize(corePoolSize + 5);
            log.info("增加核心线程数至: {}", corePoolSize + 5);
        }
        
        if (queueSize < 100 && corePoolSize > 10) {
            // 队列空闲,减少核心线程
            executor.setCorePoolSize(corePoolSize - 5);
            log.info("减少核心线程数至: {}", corePoolSize - 5);
        }
    }
}

七、面试高频问答💯

Q1: 四种拒绝策略的区别?

A:

  • AbortPolicy:抛异常,不丢任务,调用者感知
  • CallerRunsPolicy:调用者线程执行,背压机制
  • DiscardPolicy:静默丢弃,危险
  • DiscardOldestPolicy:丢弃最老任务,新任务优先

Q2: 生产环境推荐哪种策略?

A:

  • 默认推荐:CallerRunsPolicy(背压)
  • 重要任务:AbortPolicy + 异常处理
  • 日志等不重要任务:DiscardPolicy

Q3: 如何监控线程池拒绝情况?

A: 自定义拒绝策略,在rejectedExecution中:

  • 计数器统计拒绝次数
  • 记录日志
  • 发送告警

Q4: CallerRunsPolicy会影响性能吗?

A: 会,但这是好事(背压):

  • 调用者线程被占用,提交速度降低
  • 自然保护线程池,防止过载
  • 任务不丢失

Q5: 为什么不推荐DiscardPolicy?

A: 因为静默丢弃很危险:

  • 调用者不知道任务被丢弃
  • 可能导致数据丢失
  • 难以排查问题

八、总结:拒绝策略选型指南🎯

核心原则

  1. 不丢任务:AbortPolicy 或 CallerRunsPolicy
  2. 需要感知:AbortPolicy
  3. 自动背压:CallerRunsPolicy
  4. 可以丢失:DiscardPolicy(谨慎)
  5. 新数据优先:DiscardOldestPolicy
  6. 生产环境:自定义(告警+监控)

配置模板

// 通用模板
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    核心线程数,
    最大线程数,
    60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(队列大小),
    new ThreadFactoryBuilder()
        .setNameFormat("业务-%d")
        .setUncaughtExceptionHandler((t, e) -> log.error("线程异常", e))
        .build(),
    new CallerRunsPolicy() // 或自定义策略
);

// 添加监控
monitorThreadPool(executor);

下期预告: 如何实现一个支持超时的线程安全缓存?LRU+过期策略!🗄️