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: Не освобождаете ресурсы при ошибке или отмене.
Если канал не закрыть после ошибки или отмены, могут возникнуть утечки или блокировки файлов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ