Backend Development 15 min read

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.

Xiaokun's Architecture Exploration Notes
Xiaokun's Architecture Exploration Notes
Xiaokun's Architecture Exploration Notes
From Blocking to Non-Blocking: Evolution of Java Server IO Models

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.

JavamultithreadingReactorIOnon-blockingblocking
Xiaokun's Architecture Exploration Notes
Written by

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.

0 followers
Reader feedback

How this landed with the community

login 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.