Future.get() 在配置RejectedExecutionHandler为ThreadPoolExecutor.DiscardPolicy策略时一直阻塞

本文探讨了Java线程池中使用特定拒绝策略时出现的阻塞问题,并提供了三种解决方案,包括修改拒绝策略、使用带超时的获取方法等。

转载自:https://blog.youkuaiyun.com/doctor_who2004/article/details/115647638

1、先复现这种情况:

package com.sdcuike.java11;

import java.util.concurrent.*;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

public class Demo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("rpc-pool-%d").setDaemon(true).build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.HOURS, new SynchronousQueue<>(), threadFactory, new ThreadPoolExecutor.DiscardPolicy() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("rejectedExecution");
     
                super.rejectedExecution(r, executor);
            }
        });
     
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.HOURS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
     
        Future<String> future = threadPoolExecutor.submit(new Callable<String>() {
     
            @Override
            public String call() throws Exception {
                return "done";
            }
        });
     
        String result = future.get();
        System.out.println(result);
        System.out.println("done....");
    }

}

线程池中线程的配置为daemon线程(后台运行),当我们的main线程退出时,后台线程也会退出。会输出

done… 而现实的结果是main线程一直阻塞在future.get()调用:

img

2、why?

我们看下源码:java.util.concurrent.FutureTask#get()

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

FutureTask 内部有几种状态:

private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

当状态state<=COMPLETING 即COMPLETING和NEW两种状态时,会一直阻塞:awaitDone(false, 0L)。

FutureTask的状态设置是在线程池ThreadPoolExecutor执行过程中所设置的,一般情况下线程池队列不满,即不会执行RejectedExecutionHandler处理不能处理的任务时,状态都会设置为state > COMPLETING的某个状态 ,但线程池执行RejectedExecutionHandler的时候,java内置的拒绝策略有:

CallerRunsPolicy
public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }
    }

这种策略会使用调用线程池执行任务的当前线程执行,java.util.concurrent.FutureTask#run() 执行肯定会设置新的完成状态。

AbortPolicy
public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }

这种策略是线程池没配置的情况下使用的 默认策略,直接抛出异常,线程池根本就不会执行任务。

DiscardOldestPolicy
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

丢弃任务队列队头的任务,任何重试执行提交的新任务。状态肯定会设置。

DiscardPolicy
public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

这种策略什么都不做,任务的状态自然是初始状态,永远不会更新任务的状态,导致java.util.concurrent.FutureTask#get() 一直阻塞。

所以,最好的情况下是不使用这种策略,或者使用java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit) 带有超时的方法获取结果,或者重写策略,取消任务: ((FutureTask) r).cancel(true);

 @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                System.out.println("rejectedExecution");
                ((FutureTask) r).cancel(true)
                super.rejectedExecution(r, executor);
            }

3、解决一直阻塞的问题总结

1、重写DiscardPolicy,取消任务: ((FutureTask) r).cancel(true)。
2、不使用DiscardPolicy,建议还是用线程池默认的AbortPolicy,可以重写,打印日志,再调用super.rejectedExecution(r, executor),抛出默认异常行为。
3、使用java.util.concurrent.FutureTask#get(long, java.util.concurrent.TimeUnit) 带有超时的方法获取结果

<think>我们正在处理一个关于`RejectedExecutionException`的问题,用户遇到了错误信息:"Unexpected exception from event executor: java.util.concurrent.RejectedExecutionException: event executor terminated"。用户定义了事件,但仍然出现事件执行器终止的异常,导致事件未被接收并抛出错误。 ### 问题分析 根据引用[1]和[5],异常`RejectedExecutionException`表示线程池已经关闭,无法接受新任务。引用[2]解释了当线程池的队列已满且线程池无法处理更多任务,会触发拒绝策略,导致此异常。 具体到用户的情况,错误信息中的“event executor terminated”表明事件执行器(通常是一个线程池)已经被终止(关闭),因此当尝试提交新任务(事件)线程池拒绝执行,抛出`RejectedExecutionException`。 ### 可能原因 1. **线程池被显式关闭**:在应用程序中,可能调用了线程池的`shutdown()`或`shutdownNow()`方法,导致线程池进入关闭状态,不再接受新任务。 2. **线程池配置问题**:线程池的核心线程数、最大线程数以及队列容量配置不当,导致任务被拒绝。特别是如果线程池的工作队列已满,且线程数已达到最大值,则会触发拒绝策略。 3. **任务执行间过长**:如引用[2]所述,如果任务执行间很长(例如休眠5秒),而任务提交速度很快,可能导致队列快速填满,从而触发拒绝策略。 4. **应用程序生命周期管理问题**:在Spring等框架中,可能由于上下文关闭而导致线程池被关闭,但此仍有事件尝试提交。 ### 解决方案 #### 1. 检查线程池是否被关闭 确保在需要提交任务的整个生命周期内,线程池保持活动状态。例如,在Spring应用中,避免在容器关闭前关闭线程池。 #### 2. 调整线程池配置 根据应用负载调整线程池参数(核心线程数、最大线程数、队列容量)。例如: - 增加队列容量:使用有界队列,确保容量足够大以应对突发任务。 - 调整线程数:根据任务类型(CPU密集型或IO密集型)设置合适的线程数。 #### 3. 使用合适的拒绝策略 Java线程池提供了几种内置的拒绝策略: - `AbortPolicy`(默认):抛出`RejectedExecutionException`。 - `CallerRunsPolicy`:由调用者线程执行任务。 - `DiscardPolicy`:静默丢弃任务。 - `DiscardOldestPolicy`:丢弃队列中最旧的任务,然后重试提交。 可以通过自定义`RejectedExecutionHandler`来实现更符合业务需求的策略。 #### 4. 确保任务异常处理 如果任务中抛出未捕获的异常,可能导致工作线程退出,从而使线程池中的线程数减少,最终可能因为线程不足而拒绝新任务。因此,确保任务代码有良好的异常处理。 #### 5. 监控线程池状态 在运行监控线程池的状态(如活动线程数、队列大小等),以便及发现瓶颈。 ### 代码示例 以下是一个配置线程池并设置拒绝策略的示例: ```java import java.util.concurrent.*; public class ThreadPoolExample { public static void main(String[] args) { int corePoolSize = 5; int maxPoolSize = 10; long keepAliveTime = 5000; int queueCapacity = 100; ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(queueCapacity), new ThreadPoolExecutor.CallerRunsPolicy() // 设置拒绝策略 ); // 提交任务 for (int i = 0; i < 200; i++) { executor.submit(() -> { // 模拟任务执行 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } // 关闭线程池 executor.shutdown(); } } ``` 在这个示例中,我们使用了`CallerRunsPolicy`,当线程池饱和,由调用者线程执行任务,从而避免任务被丢弃。 ### 在Spring框架中的注意事项 如果使用Spring的线程池(如`ThreadPoolTaskExecutor`),确保正确配置: ```xml <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="5" /> <property name="maxPoolSize" value="10" /> <property name="queueCapacity" value="100" /> <property name="rejectedExecutionHandler"> <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" /> </property> </bean> ``` 或者使用Java配置: ```java @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.setThreadNamePrefix("MyExecutor-"); executor.initialize(); return executor; } ``` ### 总结 用户遇到的`RejectedExecutionException`通常是因为线程池已关闭或饱和。解决的关键在于: - 确保线程池在需要处于活动状态。 - 调整线程池配置以适应负载。 - 选择合适的拒绝策略。 通过以上措施,可以避免事件因执行器终止而被拒绝的情况。 §§相关问题§§ 1. 如何监控Java线程池的运行状态? 2. 在Spring应用中,如何优雅地关闭线程池? 3. 线程池的拒绝策略有哪些,如何选择? 4. 如何避免线程池中的任务执行异常导致线程泄露?</think>### 问题分析:事件执行器终止异常 您遇到的`RejectedExecutionException: event executor terminated`错误表示**事件执行器(线程池)已被关闭**,但仍有事件尝试提交到该线程池。引用[2]和[5]明确指出:当线程池被终止后,所有新提交的任务都会被拒绝。以下是关键原因和解决方案: --- ### 根本原因 1. **线程池提前关闭** 事件执行器(如`ThreadPoolExecutor`)可能在事件处理完成前被显式关闭(如调用`shutdown()`),或框架生命周期结束自动关闭(如Spring容器销毁)。 2. **任务积压超限** 如引用[2]所述,当任务队列已满且所有工作线程忙碌,新任务会被拒绝。若线程池配置不当(如队列容量过小或线程数不足),高并发场景易触发此问题。 3. **异步任务未等待完成** 若事件处理涉及`Future`(如引用[3]),但未正确等待任务完成就直接关闭线程池,会导致后续事件被拒绝。 --- ### 解决方案 #### 1. 检查线程池生命周期管理 - **确保线程池在事件处理期间存活** 在Spring应用中,避免在`@PreDestroy`等销毁钩子中过早关闭线程池。检查是否有类似代码: ```java @PreDestroy public void destroy() { executor.shutdown(); // 确保所有事件处理完成后再执行 } ``` - **使用`awaitTermination`等待任务完成** 关闭线程池前等待已有任务结束: ```java executor.shutdown(); if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); // 强制终止 } ``` #### 2. 优化线程池配置 根据引用[4],调整核心参数避免任务拒绝: - **增加队列容量**:使用有界队列(如`new LinkedBlockingQueue<>(1000)`)防止突发流量导致拒绝。 - **扩展线程数**:通过`setMaximumPoolSize`扩大最大线程数。 - **调整拒绝策略**:改用`CallerRunsPolicy`让调用线程直接执行任务: ```java new ThreadPoolExecutor( 10, // corePoolSize 100, // maxPoolSize 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 ); ``` #### 3. 验证事件提交机 确保事件提交发生在**线程池活跃期**。通过日志检查: ```java if (!executor.isShutdown()) { executor.submit(eventTask); // 仅在线程池未关闭提交 } else { log.error("Executor已终止,事件被拒绝"); } ``` #### 4. 异步任务监控 对于`Future`任务(引用[3]),确保所有任务完成后再关闭资源: ```java List<Future<?>> futures = new ArrayList<>(); futures.add(executor.submit(eventTask)); // 等待所有任务完成 for (Future<?> future : futures) { future.get(); // 阻塞直到任务结束 } ``` --- ### 典型修复案例 **问题代码**:事件处理中线程池被提前关闭 ```java public void processEvent(Event event) { executor.submit(() -> handleEvent(event)); executor.shutdown(); // 错误!提交后立即关闭 } ``` **修复后**: ```java public void processEvent(Event event) { if (!executor.isShutdown()) { executor.submit(() -> handleEvent(event)); } } // 在应用退出统一关闭 @PreDestroy public void safeShutdown() throws InterruptedException { executor.shutdown(); executor.awaitTermination(30, TimeUnit.SECONDS); } ``` --- ### 总结关键点 | 问题根因 | 解决措施 | |-------------------------|----------------------------------| | 线程池提前终止 | 延迟关闭 + `awaitTermination` | | 队列/线程数配置不足 | 增大队列容量或最大线程数 | | 拒绝策略不合理 | 改用`CallerRunsPolicy` | | 未处理异步任务状态 | 通过`Future.get()`等待任务完成 | > 通过上述调整,可确保事件执行器在生命周期内持续接收事件,避免`RejectedExecutionException`。若问题仍在,建议检查是否有其他组件(如连接池、框架钩子)意外关闭了线程池[^1][^5]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值