Java NIOにおけるブロッキングI/OとノンブロッキングI/Oの違い

ブロッキング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)。

タグ: Java NIO Non-blocking I/O Selector Channel

5月25日 09:42 投稿