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 的非同步 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. 在非同步 I/O 中處理錯誤與取消時的常見錯誤
錯誤 1: failed 方法在 CompletionHandler 中被忽略。
若未實作錯誤處理,應用程式行為將不可預期:錯誤會「消失」,而使用者也不知道為何沒有任何反應。
錯誤 2:操作完成後未關閉通道。
忘了關閉 AsynchronousFileChannel —— 會導致資源洩漏,甚至在作業系統鎖住檔案。
錯誤 3:在主執行緒等待非同步操作結果。
在 UI 執行緒呼叫 future.get() —— 介面會「卡住」,非同步就失去意義。
錯誤 4:嘗試取消已經完成的操作。
太晚呼叫 cancel() —— 操作已經完成,取消不會生效。這不嚴重,但在除錯時可能讓人困惑。
錯誤 5:未檢查取消結果。
呼叫了 cancel(),卻沒有檢查回傳值,也沒有在呼叫 get() 時處理 CancellationException —— 程式可能崩潰或行為怪異。
錯誤 6:在錯誤或取消時未釋放資源。
如果在錯誤或取消後不關閉通道,可能會造成資源洩漏或檔案鎖定。
GO TO FULL VERSION