Java NIO, Non-blocking I/O & Event Loop
Giải thích cơ chế readiness, Selector và event loop từ Java NIO để chuẩn bị cho Reactor Netty và Vert.x.
1. Mục tiêu
- Hiểu blocking I/O giữ execution context như thế nào.
- Hiểu Selector multiplex nhiều SelectableChannel.
- Hiểu readiness-based processing và event loop.
- Không nhầm event loop với “một thread thần kỳ”.
- Biết tại sao blocking handler phá tail latency.
2. Blocking I/O
byte[] data = socket.getInputStream().readNBytes(1024);
Nếu chưa có dữ liệu, blocking read có thể giữ thread ở trạng thái chờ. Với mô hình thread-per-connection/request, số lượng waiting connections thường kéo theo nhu cầu nhiều execution contexts.
Thread A ── read(socket A) ── WAIT Thread B ── read(socket B) ── WAIT Thread C ── read(socket C) ── WAIT
3. NIO readiness model
Java NIO cho phép channel hoạt động non-blocking và đăng ký với Selector. Selector là multiplexor của nhiều SelectableChannel.
SocketChannel A ─┐ SocketChannel B ─┼──► Selector ──► ready keys SocketChannel C ─┘
Mỗi registration được biểu diễn bằng SelectionKey. Interest set nói ta quan tâm operation nào; ready set cho biết operation nào hiện sẵn sàng.
4. Minimal Selector loop
try (Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open()) {
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select();
Iterator<SelectionKey> it =
selector.selectedKeys().iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
if (key.isAcceptable()) {
// accept connection, configure non-blocking,
// register OP_READ
} else if (key.isReadable()) {
// read available bytes without blocking the loop
}
}
}
}
5. Event Loop
while (running) {
events = waitForReadyEvents();
for (event : events) {
handle(event); // handler phải ngắn và non-blocking
}
runReadyCallbacks();
}
Event loop tận dụng việc phần lớn thời gian của network application là chờ I/O. Thay vì dành một thread cho mỗi request đang chờ, một số ít event-loop threads xử lý các event/callback khi resource đã sẵn sàng.
6. Những khái niệm không đồng nghĩa
| Phát biểu | Kết luận |
|---|---|
| Async = multi-threaded | Sai. Async mô tả quan hệ thời gian/control flow, không bắt buộc nhiều thread. |
| Non-blocking = parallel | Sai. Một event loop một thread vẫn có thể non-blocking. |
| Event loop = Reactive Streams | Sai. Event loop là execution/I/O model; Reactive Streams là protocol có backpressure. |
| Reactive = event loop | Sai. Có liên quan nhưng abstraction khác nhau. |
7. Golden Rule — Never block the event loop
t=0ms event loop xử lý Request A
t=2ms A gọi Thread.sleep(1000)
├─ Request B READY nhưng không được xử lý
├─ Request C READY nhưng không được xử lý
└─ Timer D READY nhưng không được xử lý
t=1002ms loop mới tiếp tục
Một handler block 1 giây không chỉ làm request A chậm; nó trì hoãn mọi task cùng event loop. Đó là lý do tail latency (p95/p99) có thể tăng mạnh.
8. CPU-bound work
Non-blocking I/O không giải quyết CPU saturation. Nếu handler chạy tính toán 500 ms trên event-loop thread, loop vẫn bị chiếm.
// anti-pattern trong event-loop handler
BigInteger result = expensivePrimeSearch(input);
Cần offload sang worker/compute executor hoặc thiết kế job system tùy framework.
9. Thread Pool vs Virtual Threads vs Event Loop
| Model | Execution style | Điểm mạnh | Điểm phải giữ kỷ luật |
|---|---|---|---|
| Platform pool | Blocking/imperative | Đơn giản, bounded resources | Pool/queue saturation |
| Virtual threads | Blocking/imperative | Concurrency rất lớn cho I/O wait | Downstream capacity, pinning, CPU |
| Event loop | Non-blocking/callback-Future-reactive | Ít threads cho nhiều connections | Không block loop; control flow phức tạp hơn |
10. Lab A — Blocking event loop simulator
Dùng một single-thread executor đóng vai event loop:
ExecutorService eventLoop =
Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) {
int requestId = i;
eventLoop.submit(() -> {
long start = System.currentTimeMillis();
if (requestId == 3) {
Thread.sleep(1000); // inject blocking
}
System.out.printf(
"request=%d latency=%dms thread=%s%n",
requestId,
System.currentTimeMillis() - start,
Thread.currentThread().getName()
);
return null;
});
}
Đo thời điểm request 4–19 được xử lý. Sau đó chuyển blocking task sang worker pool và post completion trở lại event loop.
11. Lab B — NIO echo server
- Tạo non-blocking
ServerSocketChannel. - Register
OP_ACCEPT. - Mỗi accepted
SocketChannelregisterOP_READ. - Khi readable, đọc vào ByteBuffer và echo.
- In số connections và thread name.
- Mở nhiều clients để quan sát một selector thread multiplex nhiều channels.
12. Lab C — Tail latency
Tạo 1.000 task rất nhanh (1 ms) xen mỗi 100 task một blocking task 100 ms. Tính:
- median/p50
- p95
- p99
- max latency
Giải thích vì sao average latency có thể trông “không quá tệ” nhưng p99 tăng mạnh.
13. Bài tập
14. Cầu nối sang Reactive Streams
Đến đây ta đã có hai vấn đề còn thiếu abstraction:
- Async/event-loop giúp không chặn thread khi chờ I/O.
- Nhưng nếu producer phát dữ liệu nhanh hơn consumer, hệ thống cần cơ chế điều tiết tốc độ.
Non-blocking I/O
+
Async callbacks/Futures
+
Demand control
↓
Reactive Streams / Backpressure
Đó là module tiếp theo trong learning path.
15. Interview checkpoint
- Selector làm gì?
- SelectionKey đại diện cho gì?
- Interest set và ready set khác nhau thế nào?
- Event loop khác thread pool?
- Tại sao blocking 100 ms có thể làm p99 tăng rất lớn?
- Non-blocking có đồng nghĩa không dùng thread không?
- Virtual Threads và Event Loop giải cùng bài toán theo hai hướng nào?
Tài liệu và code thực hành
Đọc chapter chi tiết [legacy source: modules/05-nio-event-loop.md] · Mở foundation labs [legacy source: labs/foundation/README.md] · Xem Java source [legacy source: examples/README.md]