Gymterview
middle

Что происходит, когда в потоке выбрасывается исключение?

Поведение зависит от того, перехвачено ли исключение внутри потока.

Если исключение перехвачено (caught)

Поток продолжает работу – ничего особенного не происходит. Стандартная обработка исключений Java.

Если исключение не перехвачено (uncaught)

Происходит следующая последовательность:

  1. Поток завершается (переходит в состояние TERMINATED).
  2. JVM вызывает обработчик непойманных исключений (UncaughtExceptionHandler).
  3. Если обработчик не установлен – стек-трейс выводится в System.err.
  4. Другие потоки продолжают работу – исключение в одном потоке не останавливает остальные потоки и JVM (если только это не единственный пользовательский поток).

Иерархия поиска UncaughtExceptionHandler

JVM ищет обработчик в следующем порядке:

  1. Обработчик конкретного потока: thread.getUncaughtExceptionHandler()
  2. Обработчик группы потоков: thread.getThreadGroup().uncaughtException()
  3. Обработчик по умолчанию: Thread.getDefaultUncaughtExceptionHandler()
  4. Если ни один не установлен – стек-трейс печатается в System.err

Установка обработчиков

Пример: установка UncaughtExceptionHandler
public class UncaughtExceptionDemo {
    public static void main(String[] args) throws InterruptedException {
        // Обработчик по умолчанию для ВСЕХ потоков
        Thread.setDefaultUncaughtExceptionHandler((t, e) ->
            System.err.println("[DEFAULT] Поток " + t.getName()
                + " упал: " + e.getMessage()));

        // Обработчик для конкретного потока
        Thread worker = new Thread(() -> {
            throw new RuntimeException("Что-то пошло не так!");
        }, "worker-1");

        worker.setUncaughtExceptionHandler((t, e) ->
            System.err.println("[THREAD] Поток " + t.getName()
                + " упал: " + e.getMessage()));

        worker.start();
        worker.join();

        // Поток без персонального обработчика
        Thread worker2 = new Thread(() -> {
            throw new ArithmeticException("Деление на ноль");
        }, "worker-2");

        worker2.start();
        // Сработает обработчик по умолчанию
    }
}

Особые случаи

Ситуация Поведение
Исключение в пуле потоков (execute()) Поток завершается, создаётся новый. UncaughtExceptionHandler вызывается
Исключение в пуле потоков (submit()) Исключение не выводится – оно сохраняется в Future. Вызов future.get() выбросит ExecutionException
OutOfMemoryError Поток завершается. JVM может продолжить работу, если оставшимся потокам хватает памяти
StackOverflowError Поток завершается. Другие потоки не затрагиваются

Практическая рекомендация

Всегда устанавливайте defaultUncaughtExceptionHandler для логирования:

Пример
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
    logger.error("Непойманное исключение в потоке {}", thread.getName(), throwable);
    // Отправить алерт в систему мониторинга
});

Аналогия из жизни. Непойманное исключение – это сотрудник, который ушёл с работы не предупредив. Обработчик UncaughtExceptionHandler – это охранник на проходной, который записывает в журнал, кто и почему ушёл. Без охранника никто не узнает, что сотрудник пропал.

На собеседовании. Ключевые моменты: (1) непойманное исключение завершает только один поток, не всю JVM; (2) для перехвата таких исключений существует UncaughtExceptionHandler; (3) при использовании ExecutorService.submit() исключение «прячется» в Future и может быть незамеченным – это частый источник скрытых багов. Хорошая практика – всегда вызывать future.get() или использовать execute() для задач, не возвращающих результат.