Understanding Java NIO vs IO: Channels, Buffers, and Netty Comparison

This article explains the core differences between Java NIO and traditional IO, introduces channels and buffers, details NIO’s underlying mechanisms, provides a complete code example, and compares NIO’s workflow with Netty’s model, highlighting performance and architectural distinctions.

Java Interview Crash Guide
Java Interview Crash Guide
Java Interview Crash Guide
Understanding Java NIO vs IO: Channels, Buffers, and Netty Comparison

First, the core differences:

NIO processes data in blocks, while traditional IO uses basic byte streams, making NIO generally much more efficient.

NIO no longer relies on InputStream/OutputStream; instead it uses channels and buffers built on top of streams.

NIO channels are bidirectional, whereas IO streams are unidirectional.

NIO buffers (essentially byte arrays) support slicing and can be read‑only, direct, or indirect; direct buffers are allocated specially to accelerate I/O.

The fundamental distinction between NIO and classic BIO is that NIO uses a multiplexed (non‑blocking) I/O model, while traditional IO is blocking.

First understand what channels and buffers are

What a channel means

A channel simulates the stream concept from the original I/O package; all data to any destination must pass through a Channel object. A Buffer is essentially a container object. Data sent to a channel must first be placed into a Buffer, and data read from a channel is read into a Buffer.

All data is processed via Buffer objects; you never write bytes directly to a channel, but write them into a Buffer that may contain one or more bytes. Likewise, you read bytes from a channel into a Buffer before retrieving them.

What a buffer means

A Buffer is an object that holds data to be written or that has just been read. In NIO, Buffer objects highlight a key difference from stream‑oriented I/O, where data is written directly to or read directly from a Stream.

All data in NIO is handled through buffers. When reading, data is read directly into a Buffer; when writing, data is first placed into a Buffer.

A Buffer is essentially an array—usually a byte array, though other array types exist. It provides structured access to data and can track the system’s read/write progress.

Buffer types

ByteBuffer

CharBuffer

ShortBuffer

IntBuffer

LongBuffer

FloatBuffer

DoubleBuffer

NIO underlying working principle

First, understand the buffer's working 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 the buffer is first allocated, its state is as shown in the first diagram:

After writing a few bytes, the position moves to the next free slot. To transfer the data to a channel, you call buffer.flip();:

After calling buffer.flip();, the buffer’s state becomes position → limit. Then you call clear(); to reset:

Now the operating system can correctly read the 5 bytes from the buffer and send them out. Before the next write, you call clear() again, which resets the buffer indices to their initial positions (similar to flushing a char[] buf = new char[1024]; in classic IO).

Supplement

1. mark() records the position before the current one; reset() restores the position to the marked value.

2. clear() empties the entire buffer, setting position to 0 and limit to capacity (after flip() you have remembered how many bytes were written, and clear() copies that range to the channel).

3. When writing buffer data to a channel, you call flip() to switch from write mode to read mode; flip() sets position to 0 and limit to the previous position, effectively remembering how much data is buffered.

NIO working code example

public void selector() throws IOException {
    // Allocate buffer memory
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // Open Selector to poll each Channel's state
    Selector selector = Selector.open();
    ServerSocketChannel ssc = ServerSocketChannel.open();
    ssc.configureBlocking(false); // non‑blocking
    ssc.socket().bind(new InetSocketAddress(8080));
    ssc.register(selector, SelectionKey.OP_ACCEPT); // register accept event
    while (true) {
        Set selectedKeys = selector.selectedKeys(); // get all keys
        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 request
                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();
            }
        }
    }
}

Finally, an overall NIO diagram:

NIO and Netty work model comparison

(1) 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 it, listening for OP_ACCEPT events.

The Selector enters a loop, calling select() to poll ready Channels.

When a key is OP_ACCEPT, accept the new client via ServerSocketChannel.accept().

Register the new client’s SocketChannel for OP_READ and set it non‑blocking, then remove the processed key.

If the key is not OP_ACCEPT, it must be OP_READ; invoke the data‑reading mechanism described earlier.

(2) Netty workflow steps

Create an EventLoopGroup (NIO thread group) and a ServerBootstrap.

Configure ServerBootstrap with the thread group, SO_BACKLOG option, set NioServerSocketChannel as the channel, and add business handlers.

Bind the port and start the server.

In the business handler (e.g., TimeServerHandler), read client data and respond.

(3) Differences

OP_ACCEPT handling is simplified in Netty because accept logic is uniform across business scenarios.

In NIO you manually allocate a ByteBuffer to read from a Channel; Netty reads directly into a ByteBuf, eliminating the user‑space copy from kernel to user.

Netty provides decoders such as FixedLengthFrameDecoder to handle TCP packet fragmentation, making it convenient to solve sticky‑packet issues.

Thank you for reading, hope it helps :) Source: blog.csdn.net/qq_36520235/article/details/8131818
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentNettyioChannelsJava NIOBuffers
Java Interview Crash Guide
Written by

Java Interview Crash Guide

Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.