io против nio
Классический java.io поток-ориентирован и блокирующий: читаете из stream байт за байтом, и операция блокирует поток до готовности данных — модель «один поток на соединение».
java.nio буфер-ориентирован и поддерживает неблокирующий режим: данные ходят через каналы в буферы, а поток может не ждать, а спрашивать «есть ли готовые каналы». Это позволяет одному потоку обслуживать множество соединений.
Channel и ByteBuffer
Данные в NIO ходят через Channel (FileChannel, SocketChannel) в/из ByteBuffer. У буфера три ключевых указателя: capacity (размер), position (текущая позиция) и limit (граница чтения/записи).
Главная механика — flip(): после записи в буфер (например, channel.read(buf)) перед чтением из него вызывают flip(), который ставит limit на текущую position, а position на 0. Затем clear() или compact() готовят буфер к новой записи. Забыть flip() — классическая ошибка новичка в NIO (читается мусор или ноль байт).
ByteBuffer buf = ByteBuffer.allocate(1024);
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
ch.read(buf); // пишем из канала в буфер
buf.flip(); // переключаем буфер на чтение
while (buf.hasRemaining()) process(buf.get());
}
Selector и event loop
Selector — сердце масштабируемого сетевого NIO: каналы регистрируют в нём интересующие события (готов к чтению/записи/принятию соединения), и один поток в цикле select() обрабатывает только готовые каналы. Это event loop — один (или несколько) потоков обслуживают тысячи соединений без потока на каждое.
На этом построены высоконагруженные серверы и фреймворки вроде Netty. Это объясняет, почему NIO «масштабируется лучше» для большого числа в основном простаивающих соединений, чем модель thread-per-connection.
Что изменили виртуальные потоки
Раньше выбор стоял остро: простой блокирующий код (io, поток на соединение) НЕ масштабировался, поэтому ради нагрузки писали сложный неблокирующий NIO/реактивный код. С виртуальными потоками (Java 21) блокирующий thread-per-request снова масштабируется до сотен тысяч соединений (см. отдельный урок).
Поэтому сегодня ручной non-blocking NIO с Selector нужен реже — он остался уделом высокопроизводительных фреймворков, а прикладной код часто может вернуться к простому блокирующему стилю на виртуальных потоках.
NIO.2 и off-heap
NIO.2 (java.nio.file) — это современный файловый API: Path/Files/FileSystem, обходы дерева Files.walk, ленивое Files.lines, слежение за изменениями WatchService. Им и пользуются вместо устаревшего java.io.File.
Для производительности есть direct buffers (ByteBuffer.allocateDirect — память вне кучи, меньше копирований при IO) и memory-mapped files (MappedByteBuffer — отображение файла в память для быстрого произвольного доступа). Это инструменты горячих путей, а не дефолт.
Как отвечать на собеседовании
Противопоставьте io (поток-ориентированный, блокирующий, поток-на-соединение) и nio (буфер-ориентированный, неблокирующий). Объясните связку Channel/ByteBuffer и обязательно механику flip(), и роль Selector как event loop (один поток — много соединений, основа Netty). Зрело добавьте, что виртуальные потоки вернули жизнеспособность блокирующему стилю, и упомяните NIO.2/direct buffers/mmap.