Java NIO(Non-blocking I/O)は、従来のストリームベースのI/Oパフォーマンス限界を克服するためにJava 1.4以降に導入されたAPI群です。本番環境での高スループット通信を実現する鍵は、Channel、Buffer、Selectorの3つの基盤要素を理解することにあります。
チャネル(Channel)
チャネルは双方向のデータ転送経路を提供し、同期的なストリームとは異なり、非同期操作やマルチプレクシングに対応します。すべてのチャネル操作はバッファを介して行われます。主な実装クラスとしては、ファイルアクセス用のFileChannel、TCP通信用のSocketChannelおよび接続待受用のServerSocketChannel、UDPパケット処理用のDatagramChannelが挙げられます。
バッファ(Buffer)
バッファはデータの一時的な保持領域をオブジェクト指向で抽象化したもので、メモリ領域を直接操作する代わりに安全なAPIを提供します。プリミティブ型に対応するByteBuffer、IntBuffer、CharBufferなど多数のクラスが用意されています。
核心的な制御メソッドには、メモリ確保のallocate()、データ格納のput()、データ取得のget()、および読み書きモード切替のflip()とclear()が含まれます。flip()は書き込み位置を初期化して読み取り準備を整え、clear()は状態をリセットしますが既存データを消去しません。部分的に読み取ったデータを保持しつつ新たな書き込みを行う場合はcompact()が適しています。加えて、mark()/reset()で位置を一時保存・復元でき、hasRemaining()で未処理データの存在を確認します。
内部状態は3つのポインタで管理されます。capacityは確保したメモリ上限であり変更不可です。positionは現在操作中のインデックスを示し、書き込み時は次へ進み、読み取り時はflip()実行後に0にリセットされます。limitは処理可能なデータ範囲の境界であり、書き込み時はcapacityと一致し、読み取り時は直前までのposition値を引き継ぎます。
セレクター(Selector)
セレクターはイベント駆動型のマルチプレクサとして機能し、単一のスレッドで多数のチャネルを効率的に監視します。登録時に興味のあるイベント(SelectionKey.OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE)をビット演算で指定できます。
int 監視対象 = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
登録処理はSelectionKeyインスタンスを返却し、ここでinterestOps()(希望イベント)とreadyOps()(準備完了イベント)を管理します。イベントの状態確認はビットAND演算または専用メソッド(isReadable()など)で行います。加えて、attach()/attachment()を介してコンテキスト情報を関連付け可能です。
イベントループはselect()系列メソッドで制御されます。select()はブロックし、select(timeout)は期限付き待機、selectNow()は即時チェックです。戻り値は準備完了したチャネル数です。処理対象のキーはselectedKeys()で取得し、操作完了後は手動でイテレータ経由で削除する必要があります。ブロッキング中の中断にはwakeup()を使用し、close()でリソースを解放します。
従来のI/OとNIOのコード比較
従来のストリームI/Oを用いたファイル読み込みは、ブロックベースの逐次処理が基本です。
public static void legacyFileRead() throws IOException {
Path targetPath = Paths.get("f:/io.txt");
try (InputStream stream = Files.newInputStream(targetPath)) {
byte[] chunk = new byte[1024];
int readCount = stream.read(chunk);
if (readCount > 0) {
System.out.println(new String(chunk, 0, readCount));
}
}
}
一方、NIOを利用した実装ではチャネルとバッファの連携により、メモリ効率と制御の柔軟性が向上します。
public static void nioFileRead() throws IOException {
Path targetPath = Paths.get("f:/io.txt");
try (FileChannel nioChannel = FileChannel.open(targetPath, StandardOpenOption.READ)) {
ByteBuffer workBuffer = ByteBuffer.allocate(1024);
int fetchedBytes;
while ((fetchedBytes = nioChannel.read(workBuffer)) != -1) {
workBuffer.flip();
while (workBuffer.hasRemaining()) {
System.out.print((char) workBuffer.get());
}
workBuffer.clear();
}
}
}