В следующем коде задача завершается исключением. Определите, где окажется это исключение и почему оно не выводится обработчиком необработанных исключений потока.
import java.util.concurrent.*;
class Demo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<?> future = pool.submit(() -> {
throw new IllegalStateException("boom");
});
System.out.println("done");
pool.shutdown();
}
}
Исключение не передаётся напрямую вызывающему потоку и обычно не выводится через UncaughtExceptionHandler. Метод submit() оборачивает задачу в FutureTask, которая сохраняет исключение внутри объекта Future. Оно становится видимым вызывающему коду только при вызове future.get(), где будет выброшено как причина ExecutionException.
При непосредственном запуске Thread необработанное исключение покидает метод run() и передаётся обработчику необработанных исключений потока. Исполнитель задач решает другую задачу: отделяет жизненный цикл рабочего потока от результата конкретной задачи.
Поэтому ExecutorService должен сохранить не только результат вычисления, но и исключение, чтобы вызывающий код мог получить его позже через единый интерфейс Future. Это позволяет рабочему потоку продолжить выполнение следующих задач вместо завершения из-за исключения одной задачи.
Вызов submit() сам по себе не означает, что ошибка будет немедленно замечена вызывающим потоком. Если Future не сохранить или не вызвать у него get(), задача может завершиться с ошибкой, а основной поток продолжит работу, напечатав done.
Такое поведение опасно для фоновых задач: сбой обработки сообщения, обновления кэша или периодической операции может остаться незамеченным. Вызов shutdown() лишь запрещает принимать новые задачи и не извлекает исключения из уже возвращённых Future.
submit() возвращает Future<?>. Внутренняя обёртка выполняет примерно следующую логику: при обычном завершении сохраняет результат, а при Throwable сохраняет исключение и помечает вычисление завершённым. Рабочий поток при этом не обязан завершаться.
get() ждёт завершения задачи, если она ещё выполняется. После исключительного завершения он выбрасывает ExecutionException, а исходная ошибка доступна через getCause(). Если ожидание прерывается, возникает InterruptedException; при тайм-ауте — TimeoutException.
Для execute(Runnable) поведение иное: задача не оборачивается в Future, поэтому необработанное исключение может попасть в UncaughtExceptionHandler рабочего потока, а рабочий поток обычно завершится. Это важное различие: submit() удобен для получения результата и контролируемого сбора ошибок, а execute() — для сценариев, где ошибки обрабатываются инфраструктурой исполнителя.
Нельзя бездумно ловить только ExecutionException и игнорировать причину. Ошибка может быть Error, например OutOfMemoryError, поэтому политика обработки должна явно определять, какие сбои логируются, повторяются или приводят к остановке компонента.
Сервис отправляет задачи в пул для обработки файлов и хранит возвращённые Future только для части задач. При ошибке разбора файла задача завершается, но оператор не видит сообщения: submit() захватил исключение, а get() не был вызван.
Вариант с заменой submit() на execute() позволяет использовать UncaughtExceptionHandler, но теряет унифицированный контроль результатов и усложняет ожидание завершения конкретной задачи. Вариант с вызовом get() сразу после каждого submit() делает обработку последовательной и уничтожает преимущество пула.
Практичное решение — сохранять Future и обрабатывать их после отправки всех задач, либо использовать обёртку задачи, которая централизованно логирует ошибки. Для операций, где ошибка должна немедленно влиять на вызывающий поток, вызывают get() с ограничением времени и отдельно обрабатывают ExecutionException, TimeoutException и InterruptedException.
Меняется ли поведение, если вызвать future.get() после shutdown()?
Нет. shutdown() запрещает новые отправки и позволяет уже принятым задачам завершиться. Возвращённый Future продолжает представлять ту же задачу, поэтому get() после shutdown() либо дождётся её результата, либо сообщит об исключительном завершении. Для ожидания окончания всех задач дополнительно используют awaitTermination() или сами ожидают соответствующие Future.
Что произойдёт, если вызвать future.get() и проигнорировать ExecutionException?
Если исключение поймано и проигнорировано, вызывающий код фактически подтверждает завершение задачи без успешного результата. Ошибка не будет автоматически повторно передана выше и не станет необработанным исключением основного потока. Поэтому как минимум следует записать причину в журнал, а для критичных операций — применить повтор, откат или остановку дальнейшей обработки.
Можно ли установить UncaughtExceptionHandler и гарантированно увидеть исключение от задачи, отправленной через submit()?
Нет, для стандартного submit() это не гарантируется. Исключение перехватывается оболочкой FutureTask внутри вызова run() и не покидает его как необработанное исключение потока, поэтому обработчик может не получить уведомление.
Если нужен именно UncaughtExceptionHandler, используют execute() либо собственную обёртку Runnable, которая ловит Throwable, выполняет журналирование и затем при необходимости пробрасывает исключение. При использовании submit() надёжный источник информации об ошибке — соответствующий Future и его get().