CodeGym /課程 /JAVA 25 SELF /非同步 IO: AsynchronousFileChannel (NIO2)

非同步 IO: AsynchronousFileChannel (NIO2)

JAVA 25 SELF
等級 56 , 課堂 0
開放

1. 認識非同步 IO

先來釐清術語。在經典(同步)IO 中,當你呼叫讀取或寫入方法時,你的執行緒(例如程式的主執行緒)會停下來等待,直到操作完成。這好比你打電話給朋友,在他接起電話之前,你就站在那裡盯著手機乾等。

非同步 IO(AIO)— 指的是你把讀/寫操作交給系統去做,自己則繼續處理其他工作。當操作完成時,系統會「回撥」通知你(例如呼叫你的回呼方法,或透過 Future 傳回結果)。

在哪裡需要用到?

  • 伺服器端應用程式:當磁碟在「思考」時,避免白白佔用執行緒。
  • 大量處理大檔案:避免封鎖主要執行緒。
  • 包含 UI 的應用程式:讀/寫期間避免介面「卡住」。

想像你訂了披薩。在同步的世界裡,你會站在門口等外送到來;在非同步的世界裡,你先去忙自己的事,等披薩到了,對方會打電話告訴你:「披薩到了!」

2. AsynchronousFileChannel 概覽

在 Java 中,非同步輸入/輸出自 7 版起於套件 java.nio.channels 中實作。主角是類別 AsynchronousFileChannel

它能做什麼?

  • 以非同步方式讀寫檔案資料。
  • 搭配緩衝區(ByteBuffer)運作。
  • 以不同方式取得結果:透過 Future 或透過 CompletionHandler
  • 允許明確指定用於處理事件的執行緒池(ExecutorService)。

主要方法

  • read(ByteBuffer dst, long position):會傳回 Future<Integer>
  • read(ByteBuffer dst, long position, A attachment, CompletionHandler<Integer, ? super A> handler)
  • write(ByteBuffer src, long position):會傳回 Future<Integer>
  • write(ByteBuffer src, long position, A attachment, CompletionHandler<Integer, ? super A> handler)
  • static open(Path file, Set<OpenOption> options, ExecutorService executor, FileAttribute<?>... attrs) — 開啟通道。

使用方式:

  • 透過 Future:啟動操作後,再等待其完成。
  • 透過 CompletionHandler:提供「處理器」,當操作完成(或失敗)時會被呼叫。

範例:開啟檔案以進行非同步讀/寫

import java.nio.channels.AsynchronousFileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE)
);

也可以明確指定用於處理事件的執行緒池:

import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

ExecutorService executor = Executors.newFixedThreadPool(4);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("data.txt"),
    EnumSet.of(StandardOpenOption.READ, StandardOpenOption.WRITE),
    executor
);

有趣的小知識:
如果不指定 ExecutorService,Java 會建立自己的內部執行緒池來處理 IO 事件。對於簡單任務這已足夠,但在伺服器端應用程式中,最好自行管理執行緒池。

3. ExecutorService(執行緒池)及其角色

當你使用非同步通道時,Java 需要在背後某處執行你的回呼或完成 Future。這並非魔法,而是由專用的工作執行緒來處理 — executor service。

如果你沒有傳入自己的執行緒池,Java 會直接建立內部池 — 通常是每個處理器對應一條執行緒。這很方便,但不一定安全。當你想自行掌控執行緒數量、任務優先順序與負載分配時,最好建立自己的 ExecutorService 並在 open 時傳入。

在伺服器端應用中特別重要。若沒有自有的執行緒池,很容易出現負載突增的情況 — 伺服器可能因此從平穩運作變得喘不過氣。

範例:

ExecutorService pool = Executors.newFixedThreadPool(8);

AsynchronousFileChannel channel = AsynchronousFileChannel.open(
    Path.of("huge.log"),
    EnumSet.of(StandardOpenOption.READ),
    pool
);

池的選擇之影響:

  • 執行緒很多 — 併行度更高,但系統負載也更大。
  • 執行緒很少 — 同時操作較少,但開銷較小。
  • 若要啟動成千上萬個非同步操作,務必思考平衡!

4. 實作:非同步讀取檔案

同步讀取(作為對照)

import java.nio.file.Files;
import java.nio.file.Path;

byte[] data = Files.readAllBytes(Path.of("input.txt"));
System.out.println("讀取的位元組數: " + data.length);

問題在於此處的執行緒會一直等待,直到整個檔案讀取完畢。若檔案很大或磁碟速度慢,程式就會跟著「變慢」— 其他一切在這段期間都被按下暫停。

使用 AsynchronousFileChannel 與 Future 的非同步讀取

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future;

public class AsyncReadExample {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024); // 以 1 KB 為單位讀取

            Future<Integer> result = channel.read(buffer, 0);

            // 可以同時做點別的事!
            System.out.println("已開始讀取...");

            // ... 然後再等待結果
            int bytesRead = result.get(); // 在操作完成前會封鎖執行緒

            System.out.println("讀取的位元組數: " + bytesRead);

            buffer.flip();
            // 將位元組轉為字串(若為文字檔)
            byte[] data = new byte[bytesRead];
            buffer.get(data, 0, bytesRead);
            String text = new String(data);
            System.out.println("內容: " + text);
        }
    }
}
  • channel.read(buffer, 0) — 從位置 0 啟動非同步讀取。
  • 會傳回 Future<Integer>,可用來等待結果。
  • 在操作完成之前,可以執行其他工作。
  • result.get() 會封鎖執行緒,但僅在結果尚未就緒時。

使用 CompletionHandler 的非同步讀取

(詳細內容將在下一講討論,這裡先嘗個鮮…)

import java.nio.channels.AsynchronousFileChannel;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.channels.CompletionHandler;

public class AsyncReadWithHandler {
    public static void main(String[] args) throws Exception {
        Path path = Path.of("input.txt");
        try (AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);

            channel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer bytesRead, ByteBuffer buf) {
                    buf.flip();
                    byte[] data = new byte[bytesRead];
                    buf.get(data, 0, bytesRead);
                    String text = new String(data);
                    System.out.println("非同步讀取: " + text);
                }

                @Override
                public void failed(Throwable exc, ByteBuffer buf) {
                    System.err.println("讀取錯誤: " + exc.getMessage());
                }
            });

            // 先別讓程式立刻結束(否則回呼來不及執行)
            Thread.sleep(100); // 在實務上,建議改用 latch、future 等進行同步
        }
    }
}

5. 實用細節

比較:非同步 vs 同步讀取

特性 同步 IO ( Files.readAllBytes ) 非同步 IO ( AsynchronousFileChannel )
是否封鎖執行緒 不會(若不呼叫 get()
可擴充性
適用於 UI/伺服器 不適用 適用
程式碼複雜度 簡單 稍微複雜
資源管理 簡單 務必別忘了關閉通道!

非同步 IO 的工作流程

sequenceDiagram
    participant Main as 你的執行緒
    participant OS as 作業系統
    participant Disk as 磁碟

    Main->>OS: 啟動非同步讀取 (read)
    OS->>Disk: 讀取資料
    Main->>Main: 執行其他工作
    OS-->>Main: 通知完成 (Future/CompletionHandler)
    Main->>Main: 處理結果

6. 使用 AsynchronousFileChannel 的常見錯誤

錯誤 №1:忘記關閉通道。
AsynchronousFileChannel 是需要關閉的資源。若忘了關閉通道(channel.close() 或 try-with-resources),可能導致描述元洩漏與檔案存取問題。只要可行,請一律使用 try-with-resources。

錯誤 №2:在主執行緒中呼叫會封鎖的 get()
如果你使用 Future 並在主執行緒(例如 UI 應用程式)中呼叫 get(),就失去非同步 IO 的意義 — 執行緒仍會等待。請使用 CompletionHandler,或在另一條執行緒等待結果。

錯誤 №3:不正確地使用 ByteBuffer
向緩衝區寫入後別忘了呼叫 flip(),以便將其切換為讀取模式。讀取後則呼叫 clear()compact(),若你還要重複使用該緩衝區。

錯誤 №4:忘了處理錯誤。
非同步操作可能會以錯誤結束(例如找不到檔案、沒有權限)。如果沒有在 CompletionHandler 中處理例外,或沒有檢查 Future 的錯誤,程式可能會「悄悄地」不執行操作。

錯誤 №5:未考慮的平行性。
如果你同時在同一個通道上啟動多個操作,請確保你的程式碼是執行緒安全的,且不會出現對緩衝區或檔案位置的競態條件。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION