CodeGym /课程 /JAVA 25 SELF /异步 IO 中的错误处理与操作取消

异步 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);
    }
}

这里发生了什么?

  • 如果文件不存在,将以相应的异常(NoSuchFileException)调用 failed 方法。
  • 如果操作成功完成——就会触发 completed

常见错误示例

  • 找不到文件NoSuchFileException
  • 无访问权限AccessDeniedException
  • 读/写错误:各种 IOException 子类
  • 缓冲区问题BufferOverflowExceptionBufferUnderflowException

日志记录与用户提醒

异步回调中的错误——不是惊慌失措的理由,但也不是装作什么都没发生的理由。良好的实践是记录错误日志(例如通过 Logger),如果这对用户很重要——显示消息或在 UI 中调用相应处理器。

日志记录示例:

@Override
public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("异步操作错误: " + exc);
    exc.printStackTrace();
}

在生产代码中请使用正规的日志框架(例如 java.util.loggingLog4j),而不是 System.err

2. 取消异步操作

什么时候需要取消操作?

有时需要在任务中途就把异步任务停下来。例如,用户改变主意,在下载文件期间点击了“取消”。或者程序窗口被关闭,操作也就没有意义了。又或者,在应用退出时只是需要妥善释放资源。

在这些情况下,Java 的异步 I/O 支持通过接口 Future 进行取消。借助它可以在任意时刻中断正在执行的任务,避免无谓的资源消耗。

如何使用 Future 取消操作?

AsynchronousFileChannel 中的 readwrite 方法会返回一个 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("无法取消操作(可能已经完成)");
            }
        }
    }
}

重要注意事项:

  • 取消仅对尚未完成的操作有效。
  • 如果操作已经结束——就无法取消。
  • 取消后,如果尝试在该 Future 上调用 get(),会抛出 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 MB

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

            // 50 ms 后取消操作(用于试验)
            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. 最佳实践:如何做对

即使发生错误也要释放资源

使用 try-with-resources 自动关闭通道:

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

如果使用 CompletionHandler,不要忘记在所有操作结束后关闭通道。若连续执行多次异步操作,这一点尤为重要。

不要阻塞 UI/主线程

异步操作的目的就是避免阻塞主线程。不要在 UI 线程调用 future.get() —— 否则异步就失去意义。

记录所有错误日志

CompletionHandler 中务必实现方法 failed,并记录(或向上抛出)所有异常。

在程序退出前确认所有操作都已结束

如果程序在操作结束前就退出,结果可能会丢失。演示时在控制台里有时会使用 Thread.sleep(500),但在真实应用中应使用 CountDownLatchCompletableFuture 或其他同步机制。

别忘了取消

如果操作已不再需要(例如用户关闭了窗口),通过 Future.cancel 取消它。这能节省资源并加快应用响应。

5. 在异步 IO 中进行错误处理与取消时的常见错误

错误 1:忽略方法 failed CompletionHandler
如果不实现错误处理,你的应用会表现不可预测:错误会“消失”,而用户也不明白为何什么都没有发生。

错误 2:操作结束后未关闭通道。
忘记关闭 AsynchronousFileChannel——会导致资源泄漏,甚至在操作系统中锁住文件。

错误 3:在主线程等待异步操作的结果。
在 UI 线程调用 future.get()——界面“卡住”,异步也就名存实亡。

错误 4:试图取消已完成的操作。
调用 cancel() 太晚——操作已经完成,取消不会生效。这并不致命,但会干扰调试。

错误 5:不检查取消结果。
调用 cancel() 后既不检查返回值,也不在调用 get() 时处理 CancellationException——程序可能崩溃或行为异常。

错误 6:在错误或取消时不释放资源。
如果在错误或取消后不关闭通道,可能导致资源泄漏或文件被锁。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION