轻松拿捏Java 线程池——四种内置拒绝策略(Rejected Execution Handler)

Java 线程池(java.util.concurrent.ThreadPoolExecutor)的四种内置拒绝策略(Rejected Execution Handler)。当线程池无法接受新任务时(即工作队列已满且线程数达到 maximumPoolSize),就会触发拒绝策略。

目录

核心概念回顾:

触发拒绝策略的条件:

四种内置拒绝策略:

ThreadPoolExecutor.AbortPolicy (默认策略)

ThreadPoolExecutor.CallerRunsPolicy

ThreadPoolExecutor.DiscardPolicy

ThreadPoolExecutor.DiscardOldestPolicy

总结与选择建议:

选择原则:

重要提示:


核心概念回顾:

  1. 核心线程数 (corePoolSize): 即使空闲也会保留在池中的线程数(除非设置 allowCoreThreadTimeOut)。

  2. 最大线程数 (maximumPoolSize): 池中允许存在的最大线程数。

  3. 工作队列 (workQueue): 用于存放等待执行任务的阻塞队列(如 ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue)。

  4. 拒绝策略 (RejectedExecutionHandler): 当任务提交被拒绝时(队列满 & 线程数达最大)执行的处理逻辑。

触发拒绝策略的条件:

  1. 线程池已被显式关闭 (shutdown() 或 shutdownNow())。

  2. 线程池未关闭,但:

    • 当前运行的线程数已达到 maximumPoolSize

    • 并且 工作队列已满(如果使用有界队列)。

四种内置拒绝策略:

  1. ThreadPoolExecutor.AbortPolicy (默认策略)

    • 定义与行为: 直接抛出 RejectedExecutionException 运行时异常,拒绝执行新任务。

    • 解释: 这是一种“快速失败”(Fail-Fast) 策略。它明确告知调用者当前系统负载过高,无法处理新任务。调用者需要捕获这个异常并决定如何处理(重试、记录、降级等)。

    • 适用业务场景:

      • 需要严格保证任务不丢失的场景(且调用者有能力处理失败):如关键交易处理、订单创建、支付回调等,任务失败必须被感知并记录/告警/重试。

      • 系统资源紧张,需要快速暴露问题:防止任务无限制堆积导致资源耗尽(OOM)。

      • 高并发且任务重要性高:开发者希望明确知道任务何时被拒绝,以便采取相应措施。

    • 示例代码:

      import java.util.concurrent.*;
      
      public class AbortPolicyDemo {
          public static void main(String[] args) {
              // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=AbortPolicy
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                      1, // corePoolSize
                      1, // maximumPoolSize
                      0L, TimeUnit.MILLISECONDS,
                      new ArrayBlockingQueue<>(1), // 容量为1的有界队列
                      new ThreadPoolExecutor.AbortPolicy()); // 默认策略,可显式指定
      
              // 提交3个任务 (超过队列+最大线程容量:1个运行中 + 1个在队列中 = 2,第3个触发拒绝)
              for (int i = 0; i < 3; i++) {
                  final int taskId = i;
                  try {
                      executor.execute(() -> {
                          System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
                          try {
                              Thread.sleep(1000); // 模拟任务执行
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      });
                  } catch (RejectedExecutionException e) {
                      System.err.println("Task " + taskId + " was rejected: " + e.getMessage());
                      // 在这里处理拒绝:记录日志、发送告警、尝试其他方案、重试(需谨慎)等
                  }
              }
      
              executor.shutdown(); // 记得关闭线程池
          }
      }

      输出示例:

      Task 0 executed by pool-1-thread-1
      Task 1 executed by pool-1-thread-1 // 队列中的任务在第一个任务完成后执行
      Task 2 was rejected: Task java.util.concurrent.FutureTask@... rejected from java.util.concurrent.ThreadPoolExecutor@...

  2. ThreadPoolExecutor.CallerRunsPolicy

    • 定义与行为: 被拒绝的任务不会进入线程池,而是由提交该任务的调用者线程(通常是主线程或发起调用的线程) 直接执行。

    • 解释: 这是一种“回退”(Fallback) 策略。它不会真正丢弃任务,而是让提交任务的线程自己去执行它。这有效地降低了新任务提交的速度,因为调用者线程忙于执行被拒绝的任务,暂时无法提交新任务,给线程池一个喘息的机会去处理队列中的任务。避免了任务丢失,但可能影响调用者线程的响应性。

    • 适用业务场景:

      • 任务不能丢失,且可以接受一定程度延迟或调用者线程被临时占用:如后台日志处理、非实时的数据同步、文件上传处理等。

      • 需要一种温和的方式来控制任务提交速率:防止生产者速度过快压垮消费者(线程池),起到简单的负反馈作用。

      • Web服务器请求处理(需谨慎评估):如果调用者是Tomcat的HTTP工作线程,让它直接执行任务可能会阻塞该线程处理新HTTP请求的能力,但在某些低并发或特定任务下可用。

    • 示例代码:

      import java.util.concurrent.*;
      
      public class CallerRunsPolicyDemo {
          public static void main(String[] args) {
              // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=CallerRunsPolicy
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                      1, 1, 0L, TimeUnit.MILLISECONDS,
                      new ArrayBlockingQueue<>(1),
                      new ThreadPoolExecutor.CallerRunsPolicy()); // 指定CallerRunsPolicy
      
              System.out.println("Main thread: " + Thread.currentThread().getName());
      
              // 提交3个任务
              for (int i = 0; i < 3; i++) {
                  final int taskId = i;
                  executor.execute(() -> {
                      System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  });
              }
      
              executor.shutdown();
          }
      }

      输出示例:

      Main thread: main
      Task 0 executed by pool-1-thread-1 // 线程池线程执行
      Task 1 executed by pool-1-thread-1 // 队列中的任务被线程池线程执行
      Task 2 executed by main // 第三个任务被拒绝,由主线程(main)自己执行!
  3. ThreadPoolExecutor.DiscardPolicy

    • 定义与行为: 默默地、直接丢弃 被拒绝的新任务,不做任何通知,也不抛出异常。

    • 解释: 这是一种“静默丢弃”(Silent Drop) 策略。任务被丢弃如同从未提交过一样。调用者完全不知道任务被丢弃了。风险在于重要任务可能无声无息地丢失

    • 适用业务场景:

      • 允许任务丢失且无需通知的场景:如不重要的监控采样数据、实时性要求不高且可容忍丢失的统计信息(如页面点击计数,丢几个点没关系)、频繁但非关键的心跳检测。

      • 任务具有“可丢弃”属性:例如,如果同一个用户短时间内提交了多个相同的更新状态请求,丢弃掉一些旧的或中间的请求可能可以接受。

      • 需要避免因拒绝任务导致上层逻辑中断(如避免抛出异常),且业务逻辑本身对任务丢失有容忍度。

    • 示例代码:

      import java.util.concurrent.*;
      
      public class DiscardPolicyDemo {
          public static void main(String[] args) {
              // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=1, 拒绝策略=DiscardPolicy
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                      1, 1, 0L, TimeUnit.MILLISECONDS,
                      new ArrayBlockingQueue<>(1),
                      new ThreadPoolExecutor.DiscardPolicy()); // 指定DiscardPolicy
      
              // 提交3个任务
              for (int i = 0; i < 3; i++) {
                  final int taskId = i;
                  executor.execute(() -> {
                      System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  });
                  System.out.println("Task " + taskId + " submitted.");
              }
      
              executor.shutdown();
          }
      }

      输出示例:

      Task 0 submitted.
      Task 0 executed by pool-1-thread-1
      Task 1 submitted. // Task1 进入队列
      Task 2 submitted. // Task2 被提交时触发拒绝,被静默丢弃
      Task 1 executed by pool-1-thread-1 // 队列中的Task1被执行
      // 注意:Task2 没有任何输出,它被无声地丢弃了
  4. ThreadPoolExecutor.DiscardOldestPolicy

    • 定义与行为: 当新任务被拒绝时,它会丢弃工作队列中等待最久的(队列头部的)那个任务,然后尝试重新提交当前这个新任务到队列中。

    • 解释: 这是一种“弃旧纳新” 策略。它尝试牺牲队列中最早的任务来换取执行最新任务的机会。如果重新提交失败(可能队列在丢弃旧任务后瞬间又被新任务填满),则该新任务也会被丢弃。 有丢失任务的风险,且丢弃的是相对较旧的任务。

    • 适用业务场景:

      • 最新任务比旧任务更重要的场景:如实时状态更新(旧的坐标位置被新的覆盖)、新闻推送(最新的消息更有价值)、缓存刷新(旧缓存数据不如新数据重要)。

      • 队列中的任务存在时效性,旧任务可能已失效或价值降低:如过期的价格查询请求、基于时间窗口的统计计算(旧数据可能不需要了)。

      • 希望优先处理最新的请求,并愿意承担丢弃部分旧请求的风险。

    • 示例代码:

      import java.util.concurrent.*;
      
      public class DiscardOldestPolicyDemo {
          public static void main(String[] args) {
              // 创建线程池:核心线程数=1, 最大线程数=1, 队列容量=2, 拒绝策略=DiscardOldestPolicy
              ThreadPoolExecutor executor = new ThreadPoolExecutor(
                      1, 1, 0L, TimeUnit.MILLISECONDS,
                      new ArrayBlockingQueue<>(2), // 容量为2的队列
                      new ThreadPoolExecutor.DiscardOldestPolicy()); // 指定DiscardOldestPolicy
      
              // 提交4个任务 (超过容量:1运行 + 2队列 = 3,第4个触发拒绝)
              for (int i = 0; i < 4; i++) {
                  final int taskId = i;
                  executor.execute(() -> {
                      System.out.println("Task " + taskId + " STARTED by " + Thread.currentThread().getName());
                      try {
                          Thread.sleep(2000); // 延长任务执行时间,方便观察队列变化
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println("Task " + taskId + " FINISHED");
                  });
                  System.out.println("Task " + taskId + " submitted.");
              }
      
              try {
                  Thread.sleep(1000); // 稍微等待一下,让任务有机会进入队列
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              // (可选) 此时可以检查队列,队列里应该是[Task1, Task2],Task0在运行。
              // 提交Task3时触发拒绝:丢弃队列头的Task1,尝试将Task3加入队列尾。
      
              executor.shutdown();
          }
      }

      输出示例:

      Task 0 submitted.
      Task 0 STARTED by pool-1-thread-1 // 核心线程执行Task0
      Task 1 submitted. // Task1进入队列
      Task 2 submitted. // Task2进入队列
      Task 3 submitted. // 提交Task3时触发拒绝策略
      // 策略动作:丢弃队列头部任务(Task1),尝试重新提交Task3
      Task 0 FINISHED
      Task 3 STARTED by pool-1-thread-1 // 线程池线程取出并执行队列中新的队头任务Task3 (Task2还在队列里)
      Task 3 FINISHED
      Task 2 STARTED by pool-1-thread-1 // 线程池线程执行队列中最后的Task2
      Task 2 FINISHED
      // 注意:Task1 被丢弃了,它既没有 STARTED 也没有 FINISHED 输出

总结与选择建议:

策略 (RejectedExecutionHandler)行为是否丢失任务是否通知调用者特点/风险适用场景
AbortPolicy (默认)抛出 RejectedExecutionException (异常)快速失败,强制调用者处理失败。可能导致业务中断。任务关键,不允许静默丢失,需要明确感知失败。
CallerRunsPolicy调用者线程自己执行 (感知执行变慢)不丢弃任务,但降低提交速度。可能阻塞调用者线程。任务重要可延迟,需控制提交速率,避免任务丢失。
DiscardPolicy默默丢弃新任务完全静默。重要任务丢失无感知。允许静默丢失的非关键任务(如采样、统计、日志)。
DiscardOldestPolicy丢弃队列头任务,重试提交新任务是 (丢弃旧任务)弃旧纳新。可能丢失有价值的旧任务。最新任务比旧任务更重要,旧任务可能已过时。(实时数据处理系统)

选择原则:

  1. 任务重要性: 绝对不能丢?用 CallerRunsPolicy 或自定义策略(如持久化)。可以丢?考虑 Discard* 或 Abort

  2. 业务容忍度: 能接受失败告警?用 AbortPolicy。能接受延迟?用 CallerRunsPolicy。能接受静默丢失?用 DiscardPolicy。新任务优先?用 DiscardOldestPolicy

  3. 系统目标: 需要快速暴露问题?选 AbortPolicy。需要系统持续运行即使牺牲部分任务?选 Discard* 或 CallerRunsPolicy

  4. 调用者影响: 调用者线程能否被占用执行任务?能则 CallerRunsPolicy 可行。不能则避免。

  5. 队列性质: 如果队列是无界的(不推荐),理论上不会触发拒绝策略(除非关闭)。有界队列的大小直接影响触发拒绝的难易程度。

重要提示:

  • 默认策略是 AbortPolicy

  • 可以通过 ThreadPoolExecutor 的构造函数或 setRejectedExecutionHandler 方法设置拒绝策略。

  • 如果内置策略都不满足需求,可以实现 RejectedExecutionHandler 接口定义自己的拒绝逻辑(如将任务记录到日志、存入数据库稍后重试、转发到另一个队列等)。

  • 合理配置 corePoolSizemaximumPoolSize 和 workQueue 的大小是避免频繁触发拒绝策略的根本。需要根据具体业务负载和机器资源进行调优。

  • 务必记得在不再需要线程池时调用 shutdown() 或 shutdownNow() 来优雅关闭,释放资源。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值