【问题标题】:RejectedExecutionException inside single executor service单个执行器服务中的 RejectedExecutionException
【发布时间】:2020-03-02 01:00:06
【问题描述】:

在我们的一项服务中,有人添加了这样(简化的)一段代码:

public class DeleteMe {

    public static void main(String[] args) {

        DeleteMe d = new DeleteMe();
        for (int i = 0; i < 10_000; ++i) {
            d.trigger(i);
        }
    }

    private Future<?> trigger(int i) {

        ExecutorService es = Executors.newSingleThreadExecutor();
        Future<?> f = es.submit(() -> {
            try {
                // some long running task
                Thread.sleep(10_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        return f;
    }
}

有时会失败:

Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@3148f668 rejected from java.util.concurrent.ThreadPoolExecutor@6e005dc9[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
    at java.util.concurrent.Executors$DelegatedExecutorService.submit(Executors.java:678)
    at com.erabii.so.DeleteMe.trigger(DeleteMe.java:29)
    at com.erabii.so.DeleteMe.main(DeleteMe.java:22)

大多数时候错误是OutOfMemoryError - 我完全理解。编写代码的人从未调用过ExecutorService::shutDown,因此使其保持活跃。当然,为每个方法调用创建一个单独的执行器服务是不好的,并且会被更改;但这正是出现错误的原因。

我不明白的一点是为什么RejectedExecutionException会被抛出,特别是它被抛出here

代码 cmets there 有点意思:

  1. 如果我们不能排队任务,那么我们尝试添加一个新线程。如果它失败了,我们知道我们已经关闭或饱和,因此拒绝任务。

如果真的是这样,execute的文档怎么没有提到这个?

如果任务无法提交执行,要么是因为这个执行器已经关闭,要么是因为它的容量已经达到,任务由当前的 RejectedExecutionHandler 处理。

坦率地说,最初我虽然 ExecutorService 是 GC-ed - 可达性和范围是不同的东西,并且允许 GC 清除任何 可达的东西;但是有一个Future&lt;?&gt; 会保留对该服务的强烈引用,所以我排除了这个。

【问题讨论】:

  • “但是有一个 Future&lt;?&gt; 将保持对该服务的强引用” - Future 只是由该方法返回,但没有在其他任何地方使用/存储。也许 JVM 看到了这一点并确定它无法访问?
  • @JacobG。如前所述 - 这是简化的它是在调用者中使用的,当然。
  • @JacobG。还要注意pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0 - 这意味着 nothing 尚未提交到队列中,这发生在主线程中。所以主线程(不是池中的那个)必须将任务放入队列中,然后然后让执行到一个不同的线程——这甚至没有发生在这里。
  • 如果您使用newFixedThreadPool(1) 而不是newSingleThreadExecutor(),您是否会遇到相同的行为?

标签: java java-8 executorservice


【解决方案1】:

你写的

坦率地说,最初我虽然 ExecutorService 是 GC-ed - 可达性和范围是不同的东西,并且允许 GC 清除任何 可达的东西;但是有一个Future&lt;?&gt; 会保留对该服务的强烈引用,所以我排除了它。

但这实际上是一个非常合理的场景,在JDK-8145304 中有描述。在错误报告的示例中,ExecutorService 没有保存在局部变量中,但局部变量本身并不能阻止垃圾收集。

注意异常信息

Task java.util.concurrent.FutureTask@3148f668 rejected from  
    java.util.concurrent.ThreadPoolExecutor@6e005dc9[Terminated,
        pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0]

支持这一点,因为ThreadPoolExecutor@6e005dc9 的状态被指定为Terminated

期货持有对其创建ExecutorService的引用的假设是错误的。实际类型取决于服务实现,但对于常见的,它将是FutureTask 的实例,它没有引用ExecutorService。在适用于您的案例的异常消息中也可以看到。

即使它有引用,创建者也将是实际的ThreadPoolExecutor,但它是包装FinalizableDelegatedExecutorService 实例,它会收集垃圾并在ThreadPoolExecutor 实例上调用shutdown()(薄包装通常很好在绕过包装的优化代码中过早进行垃圾收集的候选者)。

请注意,虽然错误报告仍处于打开状态,但问题实际上已在 JDK 11 中得到修复。在那里,FinalizableDelegatedExecutorService 的基类 DelegatedExecutorService 有一个 execute 实现,如下所示:

public void execute(Runnable command) {
    try {
        e.execute(command);
    } finally { reachabilityFence(this); }
}

【讨论】:

  • 我和我的同事在发布问题之前得出了几乎相同的结论,但提供所有这些细节会很糟糕。非常感谢您确认!
猜你喜欢
  • 2017-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-31
  • 1970-01-01
  • 2021-08-03
  • 2011-08-02
  • 2020-12-12
  • 2018-03-01
相关资源
最近更新 更多