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 子类
- 缓冲区问题: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 的异步 I/O 支持通过接口 Future 进行取消。借助它可以在任意时刻中断正在执行的任务,避免无谓的资源消耗。
如何使用 Future 取消操作?
AsynchronousFileChannel 中的 read 或 write 方法会返回一个 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),但在真实应用中应使用 CountDownLatch、CompletableFuture 或其他同步机制。
别忘了取消
如果操作已不再需要(例如用户关闭了窗口),通过 Future.cancel 取消它。这能节省资源并加快应用响应。
5. 在异步 IO 中进行错误处理与取消时的常见错误
错误 1:忽略方法 failed 于 CompletionHandler。
如果不实现错误处理,你的应用会表现不可预测:错误会“消失”,而用户也不明白为何什么都没有发生。
错误 2:操作结束后未关闭通道。
忘记关闭 AsynchronousFileChannel——会导致资源泄漏,甚至在操作系统中锁住文件。
错误 3:在主线程等待异步操作的结果。
在 UI 线程调用 future.get()——界面“卡住”,异步也就名存实亡。
错误 4:试图取消已完成的操作。
调用 cancel() 太晚——操作已经完成,取消不会生效。这并不致命,但会干扰调试。
错误 5:不检查取消结果。
调用 cancel() 后既不检查返回值,也不在调用 get() 时处理 CancellationException——程序可能崩溃或行为异常。
错误 6:在错误或取消时不释放资源。
如果在错误或取消后不关闭通道,可能导致资源泄漏或文件被锁。
GO TO FULL VERSION