NIO Channels and Buffers — FileChannel, ByteBuffer, SocketChannel, and Selectors
In this tutorial, you will learn about NIO Channels and Buffers. We cover key concepts, practical examples, and best practices to help you master this topic.
Java NIO channels and buffers provide high-performance I/O operations with non-blocking capabilities and direct memory access. Introduced in Java 1.4, NIO (New I/O) addresses limitations of the standard I/O stream model by adding buffer-oriented I/O, channel-based operations, and non-blocking multiplexed I/O through selectors.
What You'll Learn
- Channels: FileChannel, SocketChannel, ServerSocketChannel
- ByteBuffer: creation, reading, writing, flipping, clearing
- Non-blocking I/O with SocketChannel
- Selectors for multiplexed I/O
Why It Matters
Standard I/O streams block the calling thread. For high-performance servers handling thousands of connections, blocking I/O requires one thread per connection — which does not scale. NIO's non-blocking channels let one thread manage many connections.
Real-World Use
Web servers (Netty, Undertow), database drivers, and high-throughput network services use NIO channels. FileChannel provides memory-mapped files for extremely fast file I/O.
FileChannel
Reading a file with FileChannel:
try (FileChannel channel = FileChannel.open(
Path.of("data.bin"), StandardOpenOption.READ)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer); // reads into buffer
// ... process buffer
}
Writing:
try (FileChannel channel = FileChannel.open(
Path.of("output.bin"),
StandardOpenOption.CREATE,
StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.wrap("Hello".getBytes(StandardCharsets.UTF_8));
channel.write(buffer);
}
FileChannel Position
long position = channel.position();
channel.position(100); // seek to position 100
FileChannel Size and Truncate
long fileSize = channel.size();
channel.truncate(1024); // truncate to 1024 bytes
Memory-Mapped Files
For extremely fast file access, map a file region into memory:
try (FileChannel channel = FileChannel.open(
Path.of("large.dat"), StandardOpenOption.READ)) {
MappedByteBuffer buffer = channel.map(
FileChannel.MapMode.READ_ONLY, 0, channel.size());
// Access data as if it were a byte array
byte first = buffer.get(0);
buffer.position(100);
// ...
}
Memory-mapped files can be faster than read/write because the OS handles page faults transparently.
ByteBuffer
A buffer is a fixed-size container with four key properties:
- capacity: maximum elements
- position: current read/write position
- limit: end of readable/writable area
- mark: remembered position (optional)
ByteBuffer buffer = ByteBuffer.allocate(256);
// OR from array
byte[] data = {1, 2, 3};
ByteBuffer buffer = ByteBuffer.wrap(data);
Writing and Reading
ByteBuffer buffer = ByteBuffer.allocate(8);
// Write
buffer.put((byte) 0x01);
buffer.putInt(42);
buffer.putDouble(3.14);
// Flip: switch from write mode to read mode
buffer.flip();
// Read
byte b = buffer.get();
int i = buffer.getInt();
double d = buffer.getDouble();
// Clear: reset for writing again
buffer.clear();
Compact
Moves unread data to the beginning, sets position after unread data:
buffer.compact();
// Useful when you read some data and want to write more
Direct Buffers
ByteBuffer directBuffer = ByteBuffer.allocateDirect(4096);
// Memory outside the JVM heap — faster for I/O
Direct buffers avoid copying data between JVM heap and native memory. Allocation is more expensive, but I/O operations are faster.
SocketChannel
Non-blocking network client:
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("example.com", 80));
// Wait for connection
while (!channel.finishConnect()) {
// do other work
}
// Read
ByteBuffer buffer = ByteBuffer.allocate(4096);
int bytesRead = channel.read(buffer);
// Write
buffer.flip();
channel.write(buffer);
ServerSocketChannel
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
while (true) {
SocketChannel client = server.accept(); // non-blocking
if (client != null) {
// handle client
}
}
Selectors
A Selector multiplexes multiple channels into a single thread:
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(8080));
server.configureBlocking(false);
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // blocks until at least one channel is ready
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
// accept new connection
} else if (key.isReadable()) {
// read from channel
} else if (key.isWritable()) {
// write to channel
}
keys.remove(key);
}
}
SelectionKey Operations
SelectionKey key = channel.register(selector, interestOps);
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
key.attach(Object attachment); // attach user data
Object data = key.attachment(); // retrieve attached data
key.cancel(); // unregister
Common Mistakes
- Forgetting to call
flip()before reading. After writing to a buffer, position is at the end.flip()sets limit to position and position to 0, enabling reading. - Forgetting
clear()before reusing a buffer. Without clear, position and limit are at incorrect states for writing. - Using blocking mode with Selector. Channels registered with a Selector must be in non-blocking mode.
- Not iterating over selectedKeys correctly. Always call
keys.remove(key)after processing to avoid processing the same key again. - Allocating direct buffers unnecessarily. Direct buffers are for long-lived, I/O-intensive operations. For small or infrequent I/O, use heap buffers.
Practice Questions
1. What is the difference between flip() and clear() on a ByteBuffer?
flip() switches from write mode to read mode (limit = position, position = 0). clear() resets for writing (position = 0, limit = capacity).
2. What is a direct buffer?
A buffer allocated outside the JVM heap. It is faster for I/O operations because the JVM avoids copying data between heap and native memory during read/write operations.
3. What does a Selector do?
A Selector monitors multiple channels for readiness events (connect, accept, read, write). It allows a single thread to manage multiple I/O channels efficiently.
4. Why must channels registered with a Selector be non-blocking?
Blocking channels would defeat the purpose of multiplexing — a blocking read on one channel would block all channels managed by the selector.
5. What is a memory-mapped file?
A file region mapped directly into virtual memory. The OS handles loading pages on demand. Reading/writing the buffer reads/writes the file directly, potentially much faster than explicit read/write calls.
Challenge Question:
Implement a simple echo server using NIO selectors. The server should accept connections, read data from clients, and echo it back. Handle multiple concurrent clients with a single thread. Use non-blocking channels and a selector.
FAQ
Mini Project
Write a program NioDemo.java that:
- Creates a file with 1 million integers using
FileChannelandByteBuffer(write in batches of 4096 bytes) - Reads them back using the same technique, measuring read time
- Creates a memory-mapped version and compares read speed
- Implements a simple non-blocking chat server using
ServerSocketChannelandSelector - The server accepts connections and broadcasts messages to all connected clients
- A simple client connects, sends a message, and receives the broadcast
- Demonstrates direct vs heap buffer allocation with timing comparison
What's Next
NIO channels handle raw bytes efficiently. But when you need to persist entire object graphs, Serialization is the standard approach. Lesson 43 covers serialization — Serializable interface, transient fields, ObjectOutputStream, and versioning with serialVersionUID.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro