Understanding Java NIO vs IO: Channels, Buffers, and Performance
This article explains the fundamental differences between Java NIO and traditional IO, introduces channels and buffers, describes NIO's internal mechanisms, provides a complete code example, and compares NIO's workflow with Netty's model for high‑performance backend development.
What are the differences between NIO and IO?
Core differences:
NIO processes data in blocks, while IO uses basic byte streams, making NIO generally much more efficient.
NIO does not rely on InputStream/OutputStream; it uses channels and buffers instead.
NIO channels are bidirectional, whereas traditional streams are unidirectional.
NIO buffers (essentially byte arrays) support slicing and can be read‑only, direct, or indirect, with direct buffers allocated specially to speed up I/O.
NIO uses a multiplexed, non‑blocking I/O model, while classic BIO uses a blocking model, giving multiplexing a clear performance advantage.
Understanding Channels and Buffers
What is a Channel?
A Channel simulates the original I/O streams; all data to or from any destination must pass through a Channel object.
Data is first placed into a Buffer before being sent to a Channel, and data read from a Channel is placed into a Buffer.
What is a Buffer?
A Buffer is an object that holds data to be written or that has just been read.
All data in NIO is processed via Buffers; reading writes directly into a Buffer, and writing takes data from a Buffer.
Although often a byte array, a Buffer provides structured access and tracks read/write progress.
Buffer Types
ByteBuffer CharBuffer ShortBuffer IntBuffer LongBuffer FloatBuffer DoubleBuffer
NIO Underlying Working Principle
Buffer Mechanism
capacity – total length of the buffer array.
position – index of the next element to be operated on.
limit – index of the first element that should not be accessed (limit ≤ capacity).
mark – remembers a previous position, default 0.
When data is written to a Buffer, buffer.flip() switches the buffer from write mode to read mode by setting position to 0 and limit to the previous position. buffer.clear() resets the buffer: position becomes 0 and limit is set to capacity, ready for the next write.
Calling mark() records the current position; reset() restores position to the marked value.
NIO Code Example
public void selector() throws IOException {
// Allocate a buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// Open a selector for multiplexing channels
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // non‑blocking mode
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
Set selectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept(); // accept client
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
it.remove();
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel sc = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = sc.read(buffer); // read data
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
}
}Comparison of NIO and Netty Working Models
NIO Workflow Steps
Create a ServerSocketChannel and a thread pool for business processing.
Bind the ServerSocketChannel to a port and set it to non‑blocking.
Open a Selector and register the ServerSocketChannel with OP_ACCEPT.
Loop: Selector.select() polls ready channels.
When a key is OP_ACCEPT, accept the new client connection.
Register the new client’s SocketChannel with OP_READ and set non‑blocking.
If the key is OP_READ, read data using the buffer mechanism described earlier.
Netty Workflow Steps
Create NIO EventLoopGroup and ServerBootstrap.
Configure ServerBootstrap: set groups, SO_BACKLOG, use NioServerSocketChannel, and add business handlers.
Bind to a port and start the server.
In the handler (e.g., TimeServerHandler), read client data and respond.
Key Differences
OP_ACCEPT handling is simplified in Netty because accept logic is standardized.
Netty reads data directly into ByteBuf, eliminating the manual ByteBuffer handling required by NIO.
Netty provides built‑in decoders like FixedLengthFrameDecoder to handle TCP packet fragmentation, which NIO requires manual handling.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
