1. 非同期I/Oの紹介
まず用語から整理しましょう。古典的な(同期)I/Oでは、読み取りや書き込みのメソッドを呼び出すと、実行中のスレッド(たとえばメインスレッド)は処理が完了するまで停止して待ちます。これは、友人に電話して相手が出るまでスマホを見つめて立ち尽くしているようなものです。
非同期I/O(AIO)では、読み書きの操作をシステムに委ね、自分の処理はそのまま続行します。操作が完了すると、コールバックメソッドが呼ばれたり、Future 経由で結果が返ってきたりして呼び戻しが来ます。
どんな場面で役立つ?
- サーバーアプリケーション: ディスクが「考えている」間にスレッドを無駄にしないため。
- 大きなファイルの大量処理: メインスレッドをブロックしないため。
- UIアプリケーション: 読み書き中にインターフェースが「フリーズ」しないようにするため。
ピザを注文したと想像してください。同期の世界では、玄関で配達員をずっと待ちます。非同期では、自分の用事を進め、ピザが届いたら「ピザが届きました!」と知らせてもらいます。
2. AsynchronousFileChannelの概要
Javaでは、非同期I/Oは java.nio.channels パッケージでJava 7以降に提供されています。主役は 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;
AsynchronousFileChannel channel = AsynchronousFileChannel.open(
Path.of("data.txt"),
StandardOpenOption.READ, StandardOpenOption.WRITE
);
イベント処理用のスレッドプールを明示的に指定することもできます:
import java.util.EnumSet;
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 は I/O イベントを処理する内部スレッドプールを作成します。簡単な用途には十分ですが、サーバーアプリケーションではプールを自前で管理する方が望ましいでしょう。
3. 実行スレッドプール(ExecutorService)とその役割
非同期チャネルを使うと、舞台裏では Java があなたのコールバックを実行したり、Future を完了させたりする必要があります。これは魔法ではなく、専用のワーカースレッド — executor service、すなわち ExecutorService によって行われます。
独自のスレッドプールを渡さない場合、Java は内部プール(通常はCPUあたり1スレッド)を生成します。便利ですが常に最適とは限りません。稼働スレッド数の制御、どのタスクを優先するか、負荷配分をどうするかを自分で管理したい場合は、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); // 1KBずつ読む
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 同期の読み取り
| 特性 | 同期I/O ( Files.readAllBytes ) | 非同期I/O ( AsynchronousFileChannel ) |
|---|---|---|
| スレッドをブロックするか | はい | いいえ(get() を呼ばなければ) |
| スケーラビリティ | 低い | 高い |
| UI/サーバーに適している | いいえ | はい |
| コードの複雑さ | 簡単 | やや複雑 |
| リソース管理 | 簡単 | チャネルのクローズを忘れないことが重要! |
非同期I/Oの動作図
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() を呼ぶと、非同期I/Oの意味が薄れ、結局スレッドは待つことになります。CompletionHandler を使うか、結果待ち専用の別スレッドを使いましょう。
エラー №3: ByteBuffer の扱いが不適切。
バッファに書き込んだ後は flip() を行い、読み取りに備えましょう。読み取った後は再利用するなら clear() や compact() を使います。
エラー №4: エラー処理の抜け。
非同期操作は失敗することがあります(例: ファイルが見つからない、アクセス権がない)。CompletionHandler で例外を処理しない、あるいは Future のエラー状態を確認しないと、プログラムは黙って操作を実行しません。
エラー №5: 想定外の並行実行。
同一チャネル上で複数の操作を同時に走らせる場合、コードがスレッドセーフであり、バッファやファイル位置を巡る競合が起きないように注意してください。
GO TO FULL VERSION