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:未考慮的平行性。
如果你同時在同一個通道上啟動多個操作,請確保你的程式碼是執行緒安全的,且不會出現對緩衝區或檔案位置的競態條件。
GO TO FULL VERSION