Как реализовать неблокирующее чтение в Java?

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

Ответ

Неблокирующее чтение позволяет выполнять операции ввода-вывода без остановки основного потока выполнения. В Java это реализуется через NIO (New I/O) API, используя три ключевых компонента:

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

Пример использования SocketChannel и Selector:

Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
// Переводим канал в неблокирующий режим
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("example.com", 80));
// Регистрируем канал в селекторе для уведомлений о готовности к чтению
channel.register(selector, SelectionKey.OP_READ);

while (true) {
    // Блокирует выполнение, пока хотя бы один из зарегистрированных каналов не будет готов
    selector.select();
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    for (SelectionKey key : selectedKeys) {
        if (key.isReadable()) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // Чтение из канала в буфер. В неблокирующем режиме может вернуть 0, если данных нет.
            int bytesRead = ((SocketChannel) key.channel()).read(buffer);
            if (bytesRead > 0) {
                buffer.flip(); // Подготовка буфера к чтению
                System.out.println(new String(buffer.array(), 0, bytesRead));
            }
        }
    }
    selectedKeys.clear(); // Очистка набора обработанных ключей
}

Почему это эффективно? Один поток (Selector) может управлять множеством соединений, экономя ресурсы по сравнению с моделью "один поток на соединение", используемой в блокирующем I/O.