Backend Development 35 min read

Understanding Java Blocking I/O, NIO, and AIO: From Per‑Connection Threads to Multiplexed and Asynchronous I/O

This article explains how traditional per‑connection thread handling (blocking I/O) wastes resources, then demonstrates Java NIO multiplexing and AIO callbacks with complete code examples, showing how they reduce thread usage and eliminate blocking while processing many simultaneous socket connections.

Java Captain
Java Captain
Java Captain
Understanding Java Blocking I/O, NIO, and AIO: From Per‑Connection Threads to Multiplexed and Asynchronous I/O

In the early days of the Internet, each client connection was handled by a dedicated thread, which is simple but wastes resources because threads spend most of their time blocked on I/O.

The article first presents a blocking I/O (BIO) example where a ServerSocket creates a new thread for every accepted Socket , and a client creates 20 sockets each with its own thread, showing the waiting and reading times.

/**
* @author lixinjie
* @since 2019-05-07
*/
public class BioServer {
static AtomicInteger counter = new AtomicInteger(0);
static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket();
ss.bind(new InetSocketAddress("localhost", 8080));
while (true) {
Socket s = ss.accept();
processWithNewThread(s);
}
} catch (Exception e) {
e.printStackTrace();
}
}
static void processWithNewThread(Socket s) {
Runnable run = () -> {
InetSocketAddress rsa = (InetSocketAddress)s.getRemoteSocketAddress();
System.out.println(time() + "->" + rsa.getHostName() + ":" + rsa.getPort() + "->" + Thread.currentThread().getId() + ":" + counter.incrementAndGet());
try {
String result = readBytes(s.getInputStream());
System.out.println(time() + "->" + result + "->" + Thread.currentThread().getId() + ":" + counter.getAndDecrement());
s.close();
} catch (Exception e) {
e.printStackTrace();
}
};
new Thread(run).start();
}
static String readBytes(InputStream is) throws Exception {
long begin = System.currentTimeMillis();
int total = 0, count = 0;
byte[] bytes = new byte[1024];
long start = 0;
while ((count = is.read(bytes)) > -1) {
if (start < 1) { start = System.currentTimeMillis(); }
total += count;
}
long end = System.currentTimeMillis();
return "wait=" + (start - begin) + "ms,read=" + (end - start) + "ms,total=" + total + "bs";
}
static String time() { return sdf.format(new Date()); }
}
/**
* @author lixinjie
* @since 2019-05-07
*/
public class Client {
public static void main(String[] args) {
try {
for (int i = 0; i < 20; i++) {
Socket s = new Socket();
s.connect(new InetSocketAddress("localhost", 8080));
processWithNewThread(s, i);
}
} catch (IOException e) { e.printStackTrace(); }
}
static void processWithNewThread(Socket s, int i) {
Runnable run = () -> {
try {
Thread.sleep((new Random().nextInt(6) + 5) * 1000);
s.getOutputStream().write(prepareBytes());
Thread.sleep(1000);
s.close();
} catch (Exception e) { e.printStackTrace(); }
};
new Thread(run).start();
}
static byte[] prepareBytes() {
byte[] bytes = new byte[1024*1024*1];
for (int i = 0; i < bytes.length; i++) { bytes[i] = 1; }
return bytes;
}
}

The article then introduces the concept of multiplexing (NIO) and uses a restaurant analogy to explain how a selector (the “runner”) can monitor many channels and only dispatch worker threads when data is ready, dramatically reducing the number of threads needed.

/**
* @author lixinjie
* @since 2019-05-07
*/
public class NioServer {
static int clientCount = 0;
static AtomicInteger counter = new AtomicInteger(0);
static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
try {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress("localhost", 8080));
while (true) {
selector.select();
Set
keys = selector.selectedKeys();
Iterator
iterator = keys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
ServerSocketChannel ssc1 = (ServerSocketChannel)key.channel();
SocketChannel sc = null;
while ((sc = ssc1.accept()) != null) {
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
InetSocketAddress rsa = (InetSocketAddress)sc.socket().getRemoteSocketAddress();
System.out.println(time() + "->" + rsa.getHostName() + ":" + rsa.getPort() + "->" + Thread.currentThread().getId() + ":" + (++clientCount));
}
} else if (key.isReadable()) {
key.interestOps(key.interestOps() & (~SelectionKey.OP_READ));
processWithNewThread((SocketChannel)key.channel(), key);
}
}
}
} catch (Exception e) { e.printStackTrace(); }
}
static void processWithNewThread(SocketChannel sc, SelectionKey key) {
Runnable run = () -> {
counter.incrementAndGet();
try {
String result = readBytes(sc);
key.interestOps(key.interestOps() | SelectionKey.OP_READ);
System.out.println(time() + "->" + result + "->" + Thread.currentThread().getId() + ":" + counter.get());
sc.close();
} catch (Exception e) { e.printStackTrace(); }
counter.decrementAndGet();
};
new Thread(run).start();
}
static String readBytes(SocketChannel sc) throws Exception {
long start = 0;
int total = 0, count = 0;
ByteBuffer bb = ByteBuffer.allocate(1024);
long begin = System.currentTimeMillis();
while ((count = sc.read(bb)) > -1) {
if (start < 1) { start = System.currentTimeMillis(); }
total += count;
bb.clear();
}
long end = System.currentTimeMillis();
return "wait=" + (start - begin) + "ms,read=" + (end - start) + "ms,total=" + total + "bs";
}
static String time() { return sdf.format(new Date()); }
}

The article continues with asynchronous I/O (AIO) where the operating system invokes callbacks when a connection is accepted or data is read, eliminating blocking entirely.

/**
* @author lixinjie
* @since 2019-05-13
*/
public class AioServer {
static int clientCount = 0;
static AtomicInteger counter = new AtomicInteger(0);
static SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
public static void main(String[] args) {
try {
AsynchronousServerSocketChannel assc = AsynchronousServerSocketChannel.open();
assc.bind(new InetSocketAddress("localhost", 8080));
assc.accept(null, new CompletionHandler
() {
@Override
public void completed(AsynchronousSocketChannel asc, Object attachment) {
assc.accept(null, this);
try {
InetSocketAddress rsa = (InetSocketAddress)asc.getRemoteAddress();
System.out.println(time() + "->" + rsa.getHostName() + ":" + rsa.getPort() + "->" + Thread.currentThread().getId() + ":" + (++clientCount));
} catch (Exception e) { }
readFromChannelAsync(asc);
}
@Override
public void failed(Throwable exc, Object attachment) { }
});
synchronized (AioServer.class) { AioServer.class.wait(); }
} catch (Exception e) { e.printStackTrace(); }
}
static void readFromChannelAsync(AsynchronousSocketChannel asc) {
ByteBuffer bb = ByteBuffer.allocate(1024*1024*1 + 1);
long begin = System.currentTimeMillis();
asc.read(bb, null, new CompletionHandler
() {
int total = 0;
@Override
public void completed(Integer count, Object attachment) {
counter.incrementAndGet();
if (count > -1) { total += count; }
int size = bb.position();
System.out.println(time() + "->count=" + count + ",total=" + total + "bs,buffer=" + size + "bs->" + Thread.currentThread().getId() + ":" + counter.get());
if (count > -1) { asc.read(bb, null, this); }
else { try { asc.close(); } catch (Exception e) { e.printStackTrace(); } }
counter.decrementAndGet();
}
@Override
public void failed(Throwable exc, Object attachment) { }
});
long end = System.currentTimeMillis();
System.out.println(time() + "->exe read req,use=" + (end - begin) + "ms->" + Thread.currentThread().getId());
}
static String time() { return sdf.format(new Date()); }
}

Performance results show that the BIO version needs 20 threads for 20 requests, the NIO version needs about 6 threads, and the AIO version needs only 3 threads, illustrating the progressive removal of blocking points.

Finally, the article classifies the three models as synchronous blocking, asynchronous blocking, and asynchronous non‑blocking, and provides guidance on choosing the appropriate model for backend Java applications.

JavaConcurrencyNIOIOMultiplexingblocking-ioaioserver-socket
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.