ブロッキングI/Oの動作メカニズム
従来のI/Oモデルでは、データの入出力処理中にスレッドが一時停止(ブロッキング)する状態に陥る。たとえば、クライアントがサーバーに対してデータを要求した際、サーバー側はそのデータが即座に利用可能かどうかを判断できないため、対応するスレッドはI/O操作が完了するまで待機状態に入る。この間、そのスレッドは他の処理を一切行えない。
この問題への初期の対策として、マルチスレッド化が採用された。複数のスレッドを用意することで、あるスレッドがI/Oでブロックされていても、別のスレッドが処理を継続できるようになる。しかし、スレッドはOSリソースを消費するため、無制限に生成することは不可能。スレッドプールのサイズには限界があり、大量の同時接続に対応しようとすると、メモリ使用量やコンテキストスイッチのオーバーヘッドが急激に増加し、システム全体のパフォーマンスが低下する。
ノンブロッキングI/Oの仕組み
Java NIOは、このような課題を解決するためにノンブロッキングI/Oを実現している。その核となるコンポーネントは以下の3つである:
- Channel(チャネル):ネットワークやファイルとの双方向通信を行うエンドポイント。
- Buffer(バッファ):データを一時的に保持するオブジェクト。読み書きの単位として使用される。
- Selector(セレクタ):複数のチャネルの状態を監視し、準備ができているチャネルだけをアプリケーションに通知する「マルチプレクサ」機能を持つ。
Selectorは、登録されたチャネルに対して、データが読める状態(OP_READ)や書き込める状態(OP_WRITE)になったときにイベントを発行する。これにより、アプリケーションは「データがあるか?」と繰り返し確認するポーリングではなく、「データが来たときに知らせる」というイベント駆動型の処理が可能になる。
実装例:クライアントとサーバーの非同期通信
以下は、ノンブロッキングI/Oを使用してメッセージを送受信するサンプルコードである。
@Test
public void clientSend() throws IOException {
// チャネルの確立
SocketChannel channel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8085));
channel.configureBlocking(false); // ノンブロッキングモードに設定
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello from client".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer); // 書き込みが完全に終わるまでループ
}
channel.close();
}
@Test
public void serverReceive() throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8085));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 接続受付イベントを監視
while (selector.select() > 0) { // 準備完了イベントがある間ループ
var keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove(); // イベント処理後は必ず削除
if (key.isAcceptable()) {
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ); // 読み取り可能になるまで待機
}
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = client.read(buf);
if (bytesRead > 0) {
buf.flip();
System.out.println("Received: " + new String(buf.array(), 0, bytesRead));
buf.clear();
} else if (bytesRead == -1) {
client.close(); // EOF → 接続終了
}
}
}
}
}
SelectionKeyによるイベント種別の管理
チャネルをセレクタに登録する際には、どの種類のI/Oイベントを監視するかを指定する必要がある。これはSelectionKeyクラスが提供する定数によって表される:
OP_READ:データの読み取り可能(値: 1)OP_WRITE:データの書き込み可能(値: 4)OP_CONNECT:接続確立完了(値: 8)OP_ACCEPT:新しい接続の受付可能(値: 16)
これらのフラグはビット演算に基づいており、複数のイベントを同時に登録することも可能である(例:OP_READ | OP_WRITE)。