From Blocking to Non-Blocking: Evolution of Java Server IO Models
This article walks through the progression of Java server‑side I/O—from classic blocking BIO, through multithreaded and thread‑pool BIO variants, to non‑blocking NIO and Reactor‑based scalable designs—explaining core concepts, code examples, and performance trade‑offs.
BIO and Multithread Design
In the previous article "Unix IO Model" we covered five IO models and their sync/async and blocking/non‑blocking concepts. Here we examine Java's IO model evolution for server‑side network programming, focusing on thread‑based designs.
Blocking IO (BIO) uses accept and read calls that block the server thread until a client connects and sends data, handling one client at a time.
<code>// server.java (partial core code)
ServerSocket server = new ServerSocket(ip, port);
while (true) {
Socket socket = server.accept(); // blocks until a client connects
out.put("Received new connection:" + socket.toString());
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = br.readLine(); // blocks until request arrives
// decode, process, encode, send ...
}
</code>Analysis:
The server blocks on accept and read , handling a single client sequentially.
CPU remains idle while waiting for client data.
To serve multiple clients, a multithreaded approach is needed.
1:1 Multithreaded BIO Model
The main thread accepts connections, while a new thread handles each client’s I/O.
<code>// thread-task.java
public class IOTask implements Runnable {
private Socket client;
public IOTask(Socket client) { this.client = client; }
public void run() {
while (!Thread.isInterrupted()) {
// read, decode, process, encode, send ...
}
}
}
// server.java
ServerSocket server = new ServerSocket(ip, port);
while (true) {
Socket client = server.accept();
out.put("Received new connection:" + client.toString());
new Thread(new IOTask(client)).start();
}
</code>Analysis:
Accept is handled by the main thread; each client request runs in its own thread.
Creating a thread per client can exhaust CPU when many connections arrive.
Thread‑Pool (M:N) BIO Model
Using a fixed thread pool reuses threads, reducing CPU overhead.
<code>// server.java
ExecutorService executors = Executors.newFixedThreadPool(MAX_THREAD_NUM);
ServerSocket server = new ServerSocket(ip, port);
while (true) {
Socket client = server.accept();
out.put("Received new connection:" + client.toString());
executors.submit(new IOTask(client));
}
</code>Analysis:
Thread pool limits the number of concurrent threads, improving resource utilization.
Blocking BIO still ties threads to waiting I/O, prompting a move to non‑blocking approaches.
NIO Design
Java NIO introduces non‑blocking channels, buffers, and selectors. The server configures channels as non‑blocking, registers them with a selector, and polls for readiness events (accept, read, write).
<code>// server.java (pseudo‑code)
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.socket().bind(new InetSocketAddress(PORT), MAX_BACKLOG_NUM);
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isAcceptable()) {
ServerSocketChannel srv = (ServerSocketChannel) key.channel();
SocketChannel client = srv.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} else if (key.isReadable()) {
// read data, process, prepare write
} else if (key.isWritable()) {
// write response, then clear write interest
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
keys.clear();
}
</code>Analysis:
Single‑threaded loop uses select() to wait for I/O readiness, reducing blocking.
Compared to BIO, NIO still relies on threads for processing logic.
Scalable IO Design – Reactor Patterns
The Reactor model separates event detection from request handling. A main reactor accepts connections and registers them with sub‑reactors, which dispatch ready events to handler threads for processing.
Single‑Reactor (single‑thread) handles all events sequentially; adding a thread pool allows concurrent handling of heavy business logic.
Multi‑Reactor splits responsibilities: the main reactor only accepts connections, while sub‑reactors manage I/O events and delegate work to worker threads, improving scalability.
Overall, the article demonstrates the evolution from simple blocking BIO to sophisticated non‑blocking, event‑driven architectures suitable for high‑performance Java web servers.
Xiaokun's Architecture Exploration Notes
10 years of backend architecture design | AI engineering infrastructure, storage architecture design, and performance optimization | Former senior developer at NetEase, Douyu, Inke, etc.
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.