CodeGym /課程 /JAVA 25 SELF /非同步 I/O 的錯誤處理與作業取消

非同步 I/O 的錯誤處理與作業取消

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 的各種子類
  • 緩衝區問題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. 在非同步 I/O 中處理錯誤與取消時的常見錯誤

錯誤 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