Что такое неблокирующий ввод-вывод (NIO) и как он работает?

«Что такое неблокирующий ввод-вывод (NIO) и как он работает?» — вопрос из категории Java Core, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Неблокирующий ввод-вывод (NIO) — это модель, при которой поток, инициировавший операцию чтения/записи, не блокируется в ожидании ее завершения. Если данные для чтения недоступны или буфер для записи заполнен, операция немедленно возвращает управление, позволяя потоку выполнять другую работу.

Ключевые компоненты Java NIO:

  • Channel — абстракция для двусторонней связи (чтение/запись). Основные реализации: SocketChannel, ServerSocketChannel, FileChannel.
  • Buffer — контейнер для данных, с которым работают каналы.
  • Selector — объект, который мониторит несколько каналов на предмет готовности к операциям ввода-вывода.

Принцип работы (мультиплексирование):

  1. Каналы переводятся в неблокирующий режим: channel.configureBlocking(false).
  2. Каналы регистрируются в Selector с указанием интересующих событий (OP_READ, OP_WRITE, OP_ACCEPT, OP_CONNECT).
  3. Поток вызывает selector.select(), который блокируется до тех пор, пока хотя бы один из зарегистрированных каналов не будет готов.
  4. Получив набор готовых каналов (selectedKeys), поток обрабатывает их по очереди.

Пример сервера:

try (ServerSocketChannel serverChannel = ServerSocketChannel.open();
     Selector selector = Selector.open()) {

    serverChannel.bind(new InetSocketAddress(8080));
    serverChannel.configureBlocking(false); // Неблокирующий режим
    serverChannel.register(selector, SelectionKey.OP_ACCEPT); // Интересует событие подключения

    while (true) {
        selector.select(); // Ожидание событий
        Set<SelectionKey> readyKeys = selector.selectedKeys();
        Iterator<SelectionKey> iter = readyKeys.iterator();

        while (iter.hasNext()) {
            SelectionKey key = iter.next();
            iter.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 buffer = ByteBuffer.allocate(256);
                client.read(buffer); // Не блокируется
                // ... обработка данных
            }
        }
    }
}

Преимущество: Один поток может эффективно обслуживать сотни или тысячи сетевых соединений, в отличие от блокирующего IO, где на каждое соединение обычно требуется отдельный поток.