【坑】: 谁教你的自定义拒绝策略不抛异常?

文章通过一个测试案例展示了在使用自定义线程池和CompletableFuture处理批量任务时,主线程被阻塞的问题。问题源于任务被拒绝执行,导致主线程在LockSupport.park后无法被unpark。调整线程池配置以增加容量可以解决问题,同时提出了解决方案,包括选择合适的拒绝策略和设置超时时间来避免长时间阻塞。

概述

最近写一个批量业务接口,为了降低响应时间,遂使用了自定义线程池 + CompletableFuture异步执行任务,阻塞获取结果,

但在测试过程中,发现一个很奇怪的问题

下面造一个测试案例来复现问题,也方便带大家了解~


问题案例

@SpringBootTest
publicclassOtherTest {
​
   @Resource
   private ThreadPoolExecutor threadPoolExecutor;
​
   @Test
   publicvoidtest() {
      List<Integer> list = new ArrayList<>(100);
      for (int i = 0; i < 100; i++) {
         list.add(i);
      }
​
      List<CompletableFuture<Integer>> futureList = list.stream().map(num -> {
         return CompletableFuture.supplyAsync(() -> {
            // 模拟业务
            return num + 1;
         }, threadPoolExecutor);
      }).collect(Collectors.toList());
​
      CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();
​
      for (CompletableFuture<Integer> completableFuture : futureList) {
         try {
            System.out.println(completableFuture.get());
         } catch (Exception e) {
            e.printStackTrace();
         }
      }
   }
​
}
复制代码

执行案例后,我们会发现主线程一直被阻塞,下面的打印根本没有执行!这 就是我遇到的问题


为啥被阻塞了?

我们直接Dump Threads,可以看到主线程处于WAITING状态,通过堆栈跟踪,我们可以看到是被LockSupport.park了

因为我们最终会CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])).join();阻塞获取结果

根据join方法一路跟踪,最终到达java.util.concurrent.CompletableFuture.Signaller#block,也符合上图的堆栈信息

ok,问题算是找到了,但是为啥会一直阻塞呢?

根据我们之前的运行结果,可以发现,打印出了任务被拒绝的error日志,难不成是因为被拒绝的原因???

想到这里,我直接调大了线程和队列容量

这里是之前的线程池配置参数

@Slf4j@ConfigurationpublicclassThreadPoolConfig {
​
   @Bean(name = "threadPoolExecutor")
   publicThreadPoolExecutorprepareThreadPoolExecutor() {
      returnnewThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS,
            newLinkedBlockingQueue<>(5), newRejectedExecutionHandler() {
         @Override
         publicvoidrejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.error("ThreadPoolExecutor 任务被拒绝,请及时检查!!!");
         }
      });
   }
​
}
复制代码

下面是调整之后的线程池参数,增加了队列容量和最大线程数

@Slf4j@ConfigurationpublicclassThreadPoolConfig {
​
   @Bean(name = "threadPoolExecutor")
   publicThreadPoolExecutorprepareThreadPoolExecutor() {
      returnnewThreadPoolExecutor(2, 10, 1, TimeUnit.SECONDS,
            newLinkedBlockingQueue<>(500), newRejectedExecutionHandler() {
         @Override
         publicvoidrejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            log.error("ThreadPoolExecutor 任务被拒绝,请及时检查!!!");
         }
      });
   }
​
}
复制代码

调整之后再次执行任务,可以看到执行成功


看源码,找问题~

通过上面我们已经了解到,是因为任务被拒绝,导致主线程LockSupport.park一直处于WAITING状态,无法继续往下执行。

既然被park了,那肯定要unpark才行

那,什么时候会unpark呢?

当然是任务都执行完之后啦,才回去unpark主线程

但之前我们通过日志看到,有些任务被拒绝了,任务被拒绝了就不会被执行呀,那阻塞获取该任务的线程就不会unpark,导致之后的任务也不执行,所以一直不能去unpark主线程,所以主线程一直被阻塞住。

下面再看看线程池的源码:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
​
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}
复制代码

线程池的八股文相信大家都知道,看上面源码我们知道,当线程数已经不小于核心线程数且队列容量也满的情况下,会再去尝试addWorker

// 此时core为falseprivate boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            returnfalse;
​
        for (;;) {
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                // 当前线程数超过核心线程数,直接返回false去触发拒绝策略
                returnfalse;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    // ...省略
    return workerStarted;
}
复制代码

addWorker中当前线程数超过核心线程数,直接返回false去触发拒绝策略,且不会把任务添加到队列中


CompletableFuture不行,那Future呢?

可能有小伙伴会说既然CompletableFuture不行,那我用Future。

其实原理上都是一样的,Future也不行

@SpringBootTest
publicclassOtherTest {
​
   @Resource
   private ThreadPoolExecutor threadPoolExecutor;
​
   @Test
   publicvoidtest() {
      List<Integer> list = new ArrayList<>(100);
      for (int i = 0; i < 100; i++) {
         list.add(i);
      }
​
      List<Future<Integer>> futureList = list.stream().map(num -> {
         return threadPoolExecutor.submit(() -> {
            return num + 1;
         });
      }).collect(Collectors.toList());
​
      for (Future<Integer> future : futureList) {
         try {
            System.out.println(future.get());
         } catch (Exception e) {
            e.printStackTrace();
         }
      }
​
   }
​
}
复制代码

使用Future同样会导致主线程park,处于WAITING状态

下面是FutureTask的get源码,会通过LockSupport.park(this);进行阻塞获取

具体解释见 -> 异步超时中断,知其然,也要知其所以然~
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}
​
private int awaitDone(boolean timed, long nanos)
  throws InterruptedException {
  long startTime = 0L;
  WaitNode q = null;
  boolean queued = false;
  for (;;) {
    int s = state;
    if (s > COMPLETING) {
      if (q != null)
        q.thread = null;
      return s;
    }
    else if (s == COMPLETING)
      Thread.yield();
    else if (Thread.interrupted()) {
      removeWaiter(q);
      throw new InterruptedException();
    }
    else if (q == null) {
      if (timed && nanos <= 0L)
        return s;
      q = new WaitNode();
    }
    else if (!queued)
      queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
    else if (timed) {
      final long parkNanos;
      if (startTime == 0L) {
        startTime = System.nanoTime();
        if (startTime == 0L)
          startTime = 1L;
        parkNanos = nanos;
      } else {
        long elapsed = System.nanoTime() - startTime;
        if (elapsed >= nanos) {
          removeWaiter(q);
          return state;
        }
        parkNanos = nanos - elapsed;
      }
      if (state < COMPLETING)
        LockSupport.parkNanos(this, parkNanos);
    }
    else
      LockSupport.park(this);
  }
}
复制代码

而,unpark时机就是在任务正常执行完后的finishCompletion,该方法会去唤醒所有阻塞获取该任务的线程

但就是任务被拒绝了,所以无法去唤醒阻塞获取该任务的线程,造就了惨案~

CompletableFuture其实也是这个原因
本质上都是因为任务被拒绝无法执行,导致无法去唤醒线程
public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
      // ...省略
    }
}
​
​
protected void set(V v) {
  if (STATE.compareAndSet(this, NEW, COMPLETING)) {
    outcome = v;
    STATE.setRelease(this, NORMAL); // final state
    // 唤醒阻塞获取该任务的线程
    finishCompletion();
  }
}
复制代码

解决方案

既然了解到坑的原因所在,那么找出对应的解决方案也不难

  1. 拒绝策略记得抛出异常,避免主线程一直等待,比如DiscardPolicy、DiscardOldestPolicy都会存在一定的问题,没有对外抛出异常。

  1. 设置超时时间,及时释放资源,比如CompletableFuture在jdk9后,可以通过orTimeout设置超时时间,Future的话,他的get方法也支持设置等待时间。

作者:Code皮皮虾

链接:https://juejin.cn/post/7214124426047750203

<think> 我们正在讨论线程池的拒绝策略,特别是如何实现自定义拒绝策略。根据之前的引用,我们知道Java提供了`RejectedExecutionHandler`接口,我们可以实现这个接口来自定义拒绝策略。用户引用的内容中提到了几种默认策略足,以及自定义策略的需求,比如在任务被拒绝时重新加入队列或控制队列增长。 自定义拒绝策略的关键步骤: 1. 实现`RejectedExecutionHandler`接口,并重写`rejectedExecution`方法。 2. 在`rejectedExecution`方法中编写自定义逻辑,例如: - 将任务重新放入队列(可能需要等待队列有空闲位置) - 记录日志或指标 - 执行降级处理 - 延迟重试等 引用[3]中提到了一个具体的需求:控制等待队列的无限制增长,当拒绝策略触发时,任务能丢弃,而是重新加入线程池,并且在队列满时主线程暂停添加新任务。这可以通过在拒绝策略中使用等待队列的put方法(阻塞方法)来实现。 引用[4]中提到了使用锁和条件变量(如`await`和`signal`)来实现阻塞等待队列有空闲位置。这实际上就是`ArrayBlockingQueue`的阻塞机制。 因此,我们可以设计一个自定义拒绝策略,当任务被拒绝时,尝试将任务重新放入队列,如果队列已满则阻塞直到有空间。但需要注意的是,这种方法可能会阻塞提交任务的线程,特别是在高并发时,可能导致提交线程大量阻塞,影响系统响应。所以需要谨慎使用。 另一种思路是,在拒绝策略中实现重试机制,例如将任务放入一个临时的队列,然后由另一个线程来重新提交(但这会增加复杂性)。 根据引用[2],还有一种做法是将任务交给调用者线程执行(类似CallerRunsPolicy),但这样也可能阻塞调用者。 下面给出一个简单的自定义拒绝策略示例,该策略在任务被拒绝时尝试重新将任务放入队列(使用阻塞方法put,会等待直到队列有空位): ```java public class RetryPutRejectedExecutionHandler implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (!executor.isShutdown()) { try { // 使用阻塞方法put,会等待直到队列有空位 executor.getQueue().put(r); } catch (InterruptedException e) { // 中断处理:恢复中断状态,以便上层代码处理 Thread.currentThread().interrupt(); // 可以选择放弃任务或者抛出异常 throw new RejectedExecutionException("Task interrupted", e); } } } } ``` 注意:这种策略可能会导致提交任务的线程被阻塞,因此适用于可以接受一定延迟的场景。另外,在等待过程中线程池可能被关闭,所以需要先检查线程池状态。 另一种自定义策略:延迟重试。例如,在任务被拒绝后,等待一段时间再尝试重新提交。这可以用ScheduledExecutorService来实现,但这样就需要额外的线程池。 实现步骤: 1. 在拒绝策略中,将任务包装成一个新的Runnable,这个Runnable会尝试重新提交原任务。 2. 如果提交失败,则使用一个定时器延时后再次尝试提交。 示例代码: ```java public class RetryRejectedExecutionHandler implements RejectedExecutionHandler { // 重试间隔(毫秒) private static final long RETRY_DELAY = 100; private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { if (!executor.isShutdown()) { // 延迟后重新尝试提交 scheduler.schedule(() -> { // 再次尝试提交 executor.submit(r); }, RETRY_DELAY, TimeUnit.MILLISECONDS); } } } ``` 但是,这种策略需要管理额外的定时线程池,并且要注意资源释放(例如在拒绝策略再使用时关闭定时线程池)。 另外,引用[5]中提到了在拒绝策略中记录线程池的状态信息,这可以帮助监控。我们可以在拒绝策略中记录日志,包括当前线程池的状态(如核心线程数、活动线程数、最大线程数、队列大小等)。 自定义拒绝策略的注意事项: 1. 避免死锁:如果自定义策略中等待队列空间释放,而提交任务的线程被阻塞,如果所有工作线程也在等待某些条件(例如需要提交任务完成才能继续),则可能导致死锁。 2. 资源管理:如果自定义策略创建了额外的线程(如定时线程池),需要确保这些资源能够被正确关闭。 3. 性能影响:自定义策略中的操作(如阻塞、重试)可能会影响系统整体性能。 总结实现方法: 1. 实现`RejectedExecutionHandler`接口。 2. 在`rejectedExecution`方法中编写自定义处理逻辑(如重试、阻塞等待、记录日志等)。 3. 将自定义拒绝策略设置到线程池中。 示例设置线程池: ```java ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueCapacity), new RetryPutRejectedExecutionHandler() // 使用自定义拒绝策略 ); ``` 最后,根据用户的问题,我们提供了两种自定义策略的实现:阻塞等待入队和延时重试。用户可以根据具体场景选择。 相关问题: 1. 如何监控线程池的拒绝策略触发情况? 2. 自定义拒绝策略中如何避免死锁? 3. 如何设计一个支持优先级任务的拒绝策略? </think> ### 实现自定义线程池拒绝策略的完整指南 #### 一、核心实现步骤 1. **实现`RejectedExecutionHandler`接口** ```java public class CustomRejectPolicy implements RejectedExecutionHandler { @Override public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) { // 自定义拒绝逻辑 if (!executor.isShutdown()) { // 示例:将任务重新加入队列(阻塞等待) try { executor.getQueue().put(task); // 阻塞直到队列有空位 } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 降级处理:记录日志或持久化任务 log.error("任务持久化失败: {}", task); } } } } ``` > 关键点:必须继承`RejectedExecutionHandler`接口并实现`rejectedExecution`方法[^1] 2. **策略绑定到线程池** ```java ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // 核心线程数 8, // 最大线程数 60, TimeUnit.SECONDS, // 空闲线程存活时间 new ArrayBlockingQueue<>(100), // 任务队列 new CustomRejectPolicy() // 绑定自定义策略 ); ``` #### 二、典型自定义策略模式 1. **阻塞重试策略** ```java public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) { while (!executor.isShutdown()) { try { if (executor.getQueue().offer(task, 500, TimeUnit.MILLISECONDS)) { break; // 成功加入队列 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 失败处理:记录或持久化 saveToRedis(task); } } } ``` > **适用场景**:金融交易、订单支付等可丢失任务系统[^3] 2. **优先级降级策略** ```java public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) { if (task instanceof PriorityTask) { PriorityTask pt = (PriorityTask) task; if (pt.getPriority() < PRIORITY_THRESHOLD) { // 低优先级任务降级处理 downgradeExecute(task); } else { // 高优先级任务尝试替换旧任务 executor.getQueue().poll(); // 移除队首 executor.execute(task); } } } ``` > **适用场景**:实时监控系统(保留高优先级数据) 3. **日志告警 + 异步重试** ```java private ScheduledExecutorService retryExecutor = Executors.newSingleThreadScheduledExecutor(); public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) { log.warn("任务被拒绝: {}", task); // 5秒后重试 retryExecutor.schedule(() -> executor.submit(task), 5, TimeUnit.SECONDS); } ``` #### 三、关键技术要点 1. **线程安全控制** - 使用`ReentrantLock`确保并发安全 - 通过`Condition`实现精确阻塞唤醒: ```java private Lock lock = new ReentrantLock(); private Condition notFull = lock.newCondition(); public void rejectedExecution(...) { lock.lock(); try { while (executor.getQueue().remainingCapacity() == 0) { notFull.await(); // 等待队列空位 } executor.execute(task); } finally { lock.unlock(); } } ``` > 引用原理:条件变量`notFull.await()`实现精准阻塞等待[^4] 2. **资源监控集成** ```java public void rejectedExecution(...) { monitor.recordRejectEvent( executor.getPoolSize(), executor.getActiveCount(), executor.getQueue().size() ); // ...后续处理 } ``` > 建议监控核心指标:活动线程数、队列大小、最大线程数等[^5] #### 四、适用场景对比 | 策略类型 | 吞吐量 | 延迟 | 数据完整性 | 典型场景 | |-------------------|--------|--------|------------|-----------------------| | 阻塞重试 | 中 | 高 | ★★★★★ | 支付系统 | | 优先级降级 | 高 | 低 | ★★★☆☆ | 实时监控 | | 异步重试 | 高 | 确定 | ★★★★☆ | 日志采集 | | 持久化存储 | 高 | 高 | ★★★★★ | 金融交易 | #### 五、生产实践建议 1. **死锁预防** ```mermaid graph LR A[提交线程] -- 获取锁 --> B[执行拒绝策略] B -- 等待队列空位 --> C[工作线程] C -- 依赖提交线程释放资源 --> A ``` 解决方案:设置超时机制 `lock.tryLock(200ms)` 2. **性能优化** - 避免在拒绝策略中同步调用远程服务 - 使用`ConcurrentHashMap`替代锁实现无锁队列 - 限制单任务重试次数(指数退避算法) 3. **资源清理** ```java @Override protected void finalize() { retryExecutor.shutdownNow(); // 防止线程泄漏 } ``` > **最佳实践**:电商大促期间采用组合策略(高优先级任务阻塞重试 + 低优先级任务异步持久化)[^3] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值