昨天我們親眼見證了同步連線的慘況:當大量併發請求湧入時,少量的執行緒會因為等待 I/O 而卡死(執行緒飢餓);但如果無腦加開成千上萬個執行緒,又會讓伺服器記憶體瞬間被撐爆,並陷入 Context Switch 的泥沼中。
既然「同步阻塞」這條路走不通,那些乘載千萬流量的科技巨頭(如 Apple、Twitter、阿里)到底是如何解決連線問題的?答案就是我們這系列的主角,也是 Java 生態系中最霸道的網路通訊框架 —— Netty
Netty 是一個基於 Java NIO (Non-blocking I/O) 的非同步、事件驅動網路應用框架。包含大家熟知的 Spring WebFlux、Elasticsearch、Cassandra 底層,全都是靠 Netty 在撐起那海量的併發連線。
它到底施了什麼魔法?我們一樣用「餐廳」的比喻來拆解它的兩大核心機制。
在昨天的傳統同步餐廳裡,一個服務生(Thread)接待一桌客人後,就必須死死盯著廚房,等菜做好了才能端給客人。10,000 個客人就需要 10,000 個服務生,效率極差。
Netty 導入了 Java NIO 中的 Selector(多工選擇器)機制。你可以把 Selector 想像成一位擁有千里眼的超強經理。
現在,服務生(Thread)幫客人點完餐、將訂單交給廚房後,不用再親自死等。經理 (Selector) 會站在櫃台,同時監控著廚房裡成千上萬張訂單的狀態。只有當某一份炸雞「真正炸好」可以出餐時,經理才會廣播通知:「第 34 號桌的餐點好了,來個人把它送過去!」
透過 Selector,伺服器不需要為每一個連線保留一個卡死的執行緒,而是用極少數的經理,就能同時監控海量連線的 I/O 狀態。
有了大堂經理監控狀態,那誰來負責送餐呢?這就是 Event Loop(事件迴圈) 的工作。
在 Netty 中,Event Loop 就是一個綁定在單一執行緒上,不斷執行 while(true) 無窮迴圈的員工。它的工作非常單純且極度高效,只做兩件事:
因為 Event Loop 處理的都是「已經準備好」的資料,所以它絕對不會發生阻塞 (Non-blocking)。一個 Event Loop 執行緒,可以在一秒內在數千個不同的客人(連線)之間來回切換處理,完美榨乾 CPU 的每一滴效能。
為了驗證這個機制,我們用 Java 內建的 CompletableFuture 來模擬 Netty Event Loop 的非同步回呼 (Callback) 機制。請仔細觀察主執行緒 (Main Thread) 這次是不是完全沒有卡住:
import java.util.concurrent.CompletableFuture;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
public class AsyncEventLoopDemo {
private static String now() {
return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
public static void main(String[] args) {
System.out.println("[" + now() + "] 餐廳開門!大堂經理 (Main Thread) 開始接客");
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 瞬間湧入 10 個使用者的上傳請求
for (int i = 1; i <= 10; i++) {
final int taskId = i;
System.out.println("[" + now() + "] 經理: 幫客人 " + taskId + " 點餐,轉交廚房");
// 模擬非同步處理:把任務交給背景的 Event Loop
CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
try {
// 模擬 S3 網路延遲
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "客人 " + taskId + " 的炸雞";
}).thenAccept(result -> {
String threadName = Thread.currentThread().getName();
System.out.println("[" + now() + "] 送餐員 (" + threadName + "): 叮咚!" + result + " 好了,送餐 V");
});
futures.add(future);
}
System.out.println("[" + now() + "] 大堂經理 (Main Thread): 10個客人點餐完畢!經理去旁邊喝咖啡了");
// 【關鍵防呆】把所有號碼牌收集起來,等待所有任務完成才結束程式
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
System.out.println("[" + now() + "] 所有客人皆已送餐完畢,餐廳完美打烊!");
}
}

仔細看這張圖的時間軸,經理 (Main Thread) 在短短幾毫秒內,一口氣幫 10 個客人點完餐,然後就去旁邊納涼了。他完全沒有被 Thread.sleep(2000) 卡住!且處理十個請求花費總執行時間也只有4秒,比前一天的同步實驗的結果9秒還要快上一倍,
為什麼花了 4 秒? 你可能會想,既然是非同步,為什麼 10 個請求需要分兩批(7 個與 3 個),總共花了 4 秒才處理完?如果真的一點都不阻塞,不是應該 2 秒就全部出餐嗎?
這就是這個模擬程式碼的破綻,也是大家最常搞混的地方!
Java 預設的 CompletableFuture 會使用 ForkJoinPool 作為背景執行緒池。因為我的電腦是 8 核心,Java 預設分配了 7 個背景送餐員(系統核心數 - 1)。
雖然經理沒有卡住,但我們在背景任務裡使用了 Thread.sleep(2000),這是一個絕對阻塞的方法。這 7 個送餐員接下前 7 份訂單後,依然在廚房裡等了 2 秒。直到他們送完前 7 份,才能回頭處理剩下的 3 個客人。
換句話說,這目前段程式碼只做到了「半套非同步」我們解放了主執行緒,但依然卡死了背景執行緒。而在真實的 Netty 底層與 AWS Async SDK 中,連背景執行緒都不會使用阻塞的 sleep 或 read!在真正的全非同步環境下,只需要 1 個 Event Loop 執行緒,就能在 2 秒內毫無阻礙地處理完這 10 個請求。後續我們會以AWS S3連線的方式來執行真正的非同步實作。
最後特別注意第 43 行的 CompletableFuture.allOf().join()。Java 預設的背景執行緒建立的都是守護執行緒 (Daemon Thread)。如果主執行緒沒等大家做完就提早下班,JVM 會直接拔掉電源,導致還在排隊的背景任務直接腰斬。確保任務的完整性,是實作非同步系統時最常踩到的地雷。
為了解決上述「背景執行緒依然被阻塞」的半套問題,真正的 Netty 底層與 AWS Async SDK 採用了業界著名的 Reactor 模型(這也是 Node.js 和 Redis 高效能的核心秘密)。連背景執行緒都不會使用阻塞的 sleep 或 read!
實務上在 Netty 的設定中,會將 Event Loop 分為兩組獨立的群組來完美分工 :
透過這種 Reactor 分工,接待員永遠不會被送餐卡住,送餐員也永遠不用在廚房罰站。在真正的全非同步環境下,只需要 1 個 Worker 執行緒,就能在 2 秒內毫無阻礙地處理完剛剛那 10 個請求。整個伺服器的連線池就可以持續高速運轉。
那麼理論課到此結束,大家準備好迎接真正的挑戰了嗎?明天,我們將打開 IDE,使用 Java 21 實作基於 Netty 與 AWS Async SDK 的 S3 非同步上傳引擎,讓大家親手感受非同步連線的威力!