单个执行器服务中的RejectedExecutionException

6

我们的其中一个服务中,某人添加了以下(简化)代码:

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,特别是在这里抛出它。
那里的代码注释有些道理:
如果我们无法排队任务,则尝试添加新线程。如果失败,则知道我们已关闭或已饱和,因此拒绝该任务。
如果确实如此,为什么execute的文档没有提到这一点?
如果任务无法提交执行,要么是因为此执行程序已关闭,要么是因为其容量已达到,任务将由当前的RejectedExecutionHandler处理。
坦率地说,最初我认为ExecutorService正在进行垃圾回收 - 可达性和作用域是不同的事情,并且GC允许清除任何可达的东西; 但是有一个Future<?>会保留对该服务的强引用,因此我排除了这一点。

1
但是有一个Future<?>将保持对该服务的强引用。Future只是由方法返回,但没有在其他任何地方使用/存储。也许JVM会看到这一点,并确定它是不可达的? - Jacob G.
@JacobG. 说过 - 这是_简化版_。当然,在调用者中使用它。 - Eugene
@JacobG. 还要注意 pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0 - 这意味着队列中还没有提交任何任务,这是在主线程中发生的。因此,主线程(而不是池中的线程)必须将任务放入队列中,然后释放执行到_不同的_线程 - 这甚至在这里都没有发生。 - Eugene
1
如果您使用newFixedThreadPool(1)而不是newSingleThreadExecutor(),是否会遇到相同的行为? - Jacob G.
1个回答

9

你写道

坦白地说,最初我认为 ExecutorService 会被GC清除 - 可达性和作用域是不同的事情,GC允许清除任何不可达的东西; 但是有一个 Future<?>将保留对该服务的强引用,因此我排除了这种可能。

但实际上,这是一种非常可能的情况,在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

认为futures持有对其创建的ExecutorService的引用是错误的。实际类型取决于服务实现,但对于常见的那些,它将是FutureTask的一个实例,它没有对ExecutorService的引用。这也可以在异常消息中看到适用于您的情况。

即使它有一个引用,创建者也将是实际的ThreadPoolExecutor,但它是包装的FinalizableDelegatedExecutorService实例会被垃圾收集并调用ThreadPoolExecutor实例上的shutdown()方法(薄包装通常是优化代码中过早垃圾收集的好选择,只需绕过包装)。

请注意,虽然bug报告仍然存在,但问题实际上已在JDK 11中修复。在那里,FinalizableDelegatedExecutorService的基类DelegatedExecutorService具有一个execute实现,如下所示:

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

1
我和我的同事在发布问题之前几乎得出了相同的结论,但提供所有这些细节可能不太好。非常感谢您的确认! - Eugene

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接