RejectedExecutionException внутри одного исполнителя - PullRequest
3 голосов
/ 05 ноября 2019

В одном из наших сервисов кто-то добавил такой (упрощенный) фрагмент кода:

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 будет выброшено, в частности, оно выбрасывается здесь .

Кодовые комментарии там иметь некоторый смысл:

Если мы не можем поставить задачу в очередь, то мы пытаемся добавить новый поток. Если это не удается, мы знаем, что мы выключены или насыщены и поэтому отклоняем задачу.

Если это действительно так, то как получается документация * 1029? * не упоминает об этом?

Если задача не может быть отправлена ​​на выполнение, либо из-за того, что этот исполнитель был закрыт, либо из-за того, что его емкость достигнута, задача обрабатывается текущим RejectedExecutionHandler.

Честно говоря, изначально я думал, что ExecutorService - это GC-ed - достижимость и область видимости - разные вещи, и GC разрешено очищать все, что не достижимо;но есть Future<?>, который будет строго ссылаться на эту службу, поэтому я исключил это.

1 Ответ

5 голосов
/ 05 ноября 2019

Вы написали

Если честно, сначала я подумал, что ExecutorService - это GC-ed - достижимость и область действия - разные вещи, и 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.

Предположение, что фьючерсы содержат ссылку на их создание ExecutorService, неверно. Фактический тип зависит от реализации службы, но для обычных это будет экземпляр FutureTask, который не имеет ссылки на ExecutorService. В сообщении об исключении также видно, что это применимо к вашему случаю.

Даже если бы у него была ссылка, создатель был бы фактическим ThreadPoolExecutor, но это инкапсулирующий экземпляр FinalizableDelegatedExecutorService, который собирает мусори вызывает shutdown() в экземпляре ThreadPoolExecutor (тонкие оболочки обычно являются хорошими кандидатами для преждевременной сборки мусора в оптимизированном коде, который просто обходит оболочку).

Обратите внимание, что, хотя отчет об ошибках все еще открыт, проблемафактически исправлено в JDK 11. Там базовый класс FinalizableDelegatedExecutorService, класс DelegatedExecutorService имеет реализацию execute, которая выглядит следующим образом:

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