JavaRush /Курсы /JAVA 25 SELF /Обработка ошибок в async IO, отмена операций

Обработка ошибок в async IO, отмена операций

JAVA 25 SELF
56 уровень , 3 лекция
Открыта

1. Обработка ошибок в асинхронных операциях

В синхронном коде всё просто: если файл не найден или нет доступа, вы тут же ловите исключение в try-catch. В асинхронном коде, особенно когда используете колбэки (CompletionHandler), ошибка может произойти уже после того, как ваш метод завершился — где-то в недрах пула потоков. Если не обработать её правильно, программа может вести себя непредсказуемо: от «тихой» потери данных до падения всего приложения.

Как ошибки передаются в CompletionHandler?

В интерфейсе CompletionHandler<V, A> есть два метода:

  • completed(V result, A attachment) — вызывается, если операция прошла успешно.
  • failed(Throwable exc, A attachment) — вызывается, если произошла ошибка.

Вот пример использования:

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncErrorDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("nonexistent.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Успешно прочитано " + result + " байт");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Ошибка чтения файла: " + exc.getMessage());
                    // Можно логировать, уведомлять пользователя, пробрасывать ошибку дальше
                }
            });
        } catch (IOException ex) {
            System.out.println("Ошибка открытия файла: " + ex.getMessage());
        }

        // Дадим время асинхронной операции завершиться (в реальных приложениях используйте CountDownLatch или другие механизмы)
        Thread.sleep(500);
    }
}

Что здесь происходит?

  • Если файл не существует, метод failed будет вызван с соответствующим исключением (NoSuchFileException).
  • Если операция завершилась успешно — сработает completed.

Примеры типичных ошибок

  • Файл не найден: NoSuchFileException
  • Нет доступа: AccessDeniedException
  • Ошибка чтения/записи: различные подклассы IOException
  • Проблемы с буфером: BufferOverflowException, BufferUnderflowException

Логирование и информирование пользователя

Ошибка в асинхронном колбэке — не повод для паники, но и не повод делать вид, что ничего не случилось. Хорошая практика — логировать ошибку (например, через Logger), а если это важно для пользователя — показать сообщение или вызвать обработчик в UI.

Пример логирования:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Ошибка асинхронной операции: " + exc);
    exc.printStackTrace();
}

В продакшн-коде используйте нормальные логгеры (например, java.util.logging или Log4j), а не System.err.

2. Отмена асинхронных операций

Когда может понадобиться отмена операции?

Иногда асинхронную задачу нужно остановить прямо посреди работы. Например, пользователь передумал и нажал «Отмена» во время загрузки файла. Или окно программы закрылось, и операция больше не имеет смысла. А бывает, что при завершении приложения просто нужно аккуратно освободить ресурсы.

Для таких случаев асинхронный ввод-вывод в Java поддерживает отмену через интерфейс Future. С его помощью можно в любой момент прервать выполняющуюся задачу и не тратить ресурсы зря.

Как отменить операцию с помощью Future?

Метод read или write в AsynchronousFileChannel возвращает объект Future<Integer>. У этого объекта есть метод cancel(boolean mayInterruptIfRunning).

Пример: отмена асинхронного чтения

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Подождём чуть-чуть, а потом отменим операцию
            Thread.sleep(100);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Операция чтения отменена!");
            } else {
                System.out.println("Не удалось отменить операцию (возможно, она уже завершена)");
            }
        }
    }
}

Важные нюансы:

  • Отмена работает только для операций, которые ещё не завершились.
  • Если операция уже закончилась — отменить её не получится.
  • После отмены, при попытке вызвать get() на этом Future, будет выброшено исключение CancellationException.

Когда отменить операцию уже нельзя?

Если задача успела завершиться — хоть успешно, хоть с ошибкой — остановить её уже невозможно, поезд ушёл.

Кроме того, не все реализации действительно умеют прерывать операции на уровне операционной системы. Например, при работе с некоторыми файловыми системами «отмена» будет чисто символической: операция продолжит выполняться, но результат вы просто проигнорируете.

3. Практика: обработка ошибок и отмена

Пример 1: обработка ошибки при чтении несуществующего файла

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.io.IOException;

public class AsyncErrorExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("no_such_file.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            channel.read(buffer, 0, buffer, new java.nio.channels.CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    System.out.println("Операция завершилась успешно");
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    System.out.println("Ошибка при чтении файла: " + exc.getClass().getSimpleName() + " - " + exc.getMessage());
                }
            });
        } catch (IOException ex) {
            System.out.println("Ошибка при открытии файла: " + ex.getMessage());
        }

        Thread.sleep(500);
    }
}

Что увидим в консоли?

Ошибка при открытии файла: no_such_file.txt

или, если ошибка возникнет именно при чтении, а не открытии:

Ошибка при чтении файла: NoSuchFileException - no_such_file.txt

Пример 2: отмена долгой операции и корректное завершение

import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.*;
import java.util.concurrent.Future;
import java.io.IOException;

public class AsyncCancelExample {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("bigfile.txt");
        ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024 * 10); // 10 МБ

        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            Future<Integer> future = channel.read(buffer, 0);

            // Через 50 мс отменяем операцию (для эксперимента)
            Thread.sleep(50);
            boolean cancelled = future.cancel(true);

            if (cancelled) {
                System.out.println("Операция чтения была отменена!");
            } else {
                System.out.println("Не удалось отменить операцию (вероятно, она уже завершилась)");
            }

            try {
                // Попробуем получить результат (бросит CancellationException)
                future.get();
            } catch (java.util.concurrent.CancellationException ex) {
                System.out.println("Поймали CancellationException: операция действительно отменена.");
            }
        }
    }
}

4. Best practices: как делать правильно

Освобождайте ресурсы даже при ошибках

Используйте try-with-resources для автоматического закрытия каналов:

try (AsynchronousFileChannel channel = /* open channel */ null) {
    // ...
}

Если используете CompletionHandler, не забудьте закрыть канал при завершении всех операций. Это особенно важно, если делаете несколько асинхронных операций подряд.

Не блокируйте UI/основной поток

Асинхронные операции нужны для того, чтобы не блокировать основной поток. Не вызывайте future.get() в UI-потоке — иначе смысл асинхронности теряется.

Логируйте все ошибки

В CompletionHandler всегда реализуйте метод failed и логируйте (или передавайте дальше) все исключения.

Проверяйте завершение всех операций перед завершением программы

Если программа завершится до того, как операция закончится, результат может быть потерян. Для демонстраций в консоли иногда приходится делать Thread.sleep(500), но в реальных приложениях используйте CountDownLatch, CompletableFuture или другие механизмы синхронизации.

Не забывайте про отмену

Если операция больше не нужна (например, пользователь закрыл окно), отменяйте её через Future.cancel. Это сэкономит ресурсы и ускорит отклик приложения.

5. Типичные ошибки при обработке ошибок и отмене в async IO

Ошибка №1: Игнорирование метода failed в CompletionHandler.
Если не реализовать обработку ошибок, ваше приложение будет вести себя непредсказуемо: ошибки «потеряются», а пользователь останется в неведении, почему ничего не происходит.

Ошибка №2: Не закрыт канал после завершения операций.
Забыли закрыть AsynchronousFileChannel — получите утечку ресурсов и, возможно, блокировку файла в ОС.

Ошибка №3: Ожидание результата асинхронной операции в главном потоке.
Вызвали future.get() в UI-потоке — интерфейс «замер», и вся асинхронность ушла коту под хвост.

Ошибка №4: Попытка отменить уже завершённую операцию.
Вызвали cancel() слишком поздно — операция уже завершилась, отмена не сработает. Это не критично, но может сбить с толку при отладке.

Ошибка №5: Не проверяете результат отмены.
Вызвали cancel(), но не проверили возвращаемое значение и не обработали CancellationException при вызове get() — программа может упасть или вести себя странно.

Ошибка №6: Не освобождаете ресурсы при ошибке или отмене.
Если канал не закрыть после ошибки или отмены, могут возникнуть утечки или блокировки файлов.

1
Задача
JAVA 25 SELF, 56 уровень, 3 лекция
Недоступна
Переключение каналов: отмена загрузки видео 🎬
Переключение каналов: отмена загрузки видео 🎬
1
Задача
JAVA 25 SELF, 56 уровень, 3 лекция
Недоступна
Мониторинг системных журналов: параллельная проверка и экстренное прекращение 🚨
Мониторинг системных журналов: параллельная проверка и экстренное прекращение 🚨
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ