CodeGym /課程 /JAVA 25 SELF /傳統執行緒 vs 虛擬執行緒:差異、優勢

傳統執行緒 vs 虛擬執行緒:差異、優勢

JAVA 25 SELF
等級 57 , 課堂 0
開放

1. 傳統執行緒:如何運作、痛點在哪裡

先回顧一下 Java 中的傳統執行緒(也稱為平台執行緒或 native threads),就是透過 new Thread(...) 建立的那些。

當你呼叫 new Thread(() -> { ... }).start(); 時,JVM 並不只是啟動一段程式碼,它會請作業系統建立一個真正的執行緒。OS 會為它配置獨立的堆疊(通常數個 MB)並保留其他服務性資源。

這樣的執行緒在任務執行期間一直存活,並佔據作業系統的執行緒表。執行緒越多,堆疊耗掉的記憶體越多,OS 的負擔就越高。因此當同時工作的執行緒很多時,應用程式可能開始「吃不消」——系統花太多時間在切換它們。

範例:傳統執行緒

Thread thread = new Thread(() -> {
    System.out.println("來自執行緒的問候!");
});
thread.start();

看起來很簡單,對吧?但如果不是建立一兩個,而是比方說一萬個這樣的執行緒——你的程式很快就會開始喘不過氣。不是記憶體用完,就是系統提示已達到執行緒上限。這不是 Java 的錯,而是架構使然:執行緒本來就「沉重而昂貴」。

為什麼會這樣?每個執行緒都會拿到自己的堆疊(通常 1–2 MB),再加上一整套由作業系統提供的輔助結構。而且 OS 也不喜歡被塞進成千上萬的執行緒——它有各種限制,而且往往相當嚴格。

即使記憶體沒用盡,也還有另一個麻煩——情境切換。當執行緒太多時,系統不斷在它們之間跳轉,保存與還原狀態。這些都需要時間並吞噬效能,因此「大量並行」不見得會帶來實際收益。

「一執行緒一請求」的問題

在舊式伺服器應用(例如 Tomcat 或 Jetty)中,常用「thread‑per‑request」模型:每個進來的使用者請求對應一個執行緒。這確實很方便,但如果你有 10_000 位使用者,就需要 10_000 個執行緒!伺服器會越來越吃力,接著你追逐的將是記憶體而非請求處理速度。

結論:
傳統執行緒適合少量並行任務,但無法有效擴展到數萬、數十萬等級。

2. 什麼是虛擬執行緒(Virtual Threads)?

這就輪到今天的主角——虛擬執行緒。它們不只是「另一種執行緒」,而是一個完全不同的架構理念。

虛擬執行緒不是由作業系統管理,而是由 JVM 自己管理。它們完全在 Java 內部實作,可以大量建立(數萬、數十萬)而不會造成記憶體膨脹與效能拖累。

重點摘要:

  • Platform Thread(平台執行緒):直接對應 OS 執行緒的傳統執行緒。
  • Virtual Thread(虛擬執行緒):由 JVM 管理、而非 OS 管理的輕量執行緒。

它怎麼運作?

虛擬執行緒是「輕量」的執行緒,不在 OS 層而是活在 JVM 內部。它們運行在少量真正的執行緒(稱為 platform threads)之上。可以把 JVM 想像成指揮:手上只有有限的樂手(真正的執行緒),但能把許多樂段(虛擬執行緒)巧妙分配給他們演奏。

架構示意:

+-------------------+           +-------------------+
|  Virtual Thread 1 |---\       |  Platform Thread  |
|  Virtual Thread 2 |---->====> |  (Carrier Thread) |
|  Virtual Thread 3 |---/       +-------------------+
         ...                        (Operating System)

Carrier Thread 是一般的 OS 執行緒,JVM 會在其上執行許多虛擬執行緒。如果某個虛擬執行緒被阻塞——例如等待磁碟或網路資料——JVM 會把它「凍結」,釋放出 carrier thread 讓其他任務使用。

為什麼這是場革命?

因為你可以繼續書寫直覺、線性的程式碼——不必塞滿 callback、CompletableFuture 以及冗長的 thenApply 鏈——同時仍能把應用擴展到成千上萬個並行操作。

虛擬執行緒只佔用數十 KB(傳統執行緒常是數個 MB),而且建立幾乎是瞬間完成。因此你可以放心地以成千上萬的規模啟動與銷毀它們,無須擔心 OS 會被拖垮。這讓 Java 的並行程式設計終於變得輕鬆而自然。

3. 虛擬執行緒的優勢

可伸縮性

有了虛擬執行緒,你可以大膽地啟動成千上萬個平行任務。比方說為每個網路請求使用一個獨立執行緒——也不必擔心伺服器會「爆掉」。

示範:100 000 個虛擬執行緒

for (int i = 0; i < 100_000; i++) {
    Thread.ofVirtual().start(() -> {
        // 這裡可以是任何邏輯
        try {
            Thread.sleep(1000); // 模擬工作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}
System.out.println("所有執行緒都已啟動!");

這段程式在一般筆電上也能輕鬆跑起來!
試著用傳統執行緒做同樣的事——你很可能會看到 OutOfMemoryError,或讓電腦直接卡成「磚」。

更容易的程式設計

虛擬執行緒讓你能維持熟悉的「阻塞式」寫法,而不會把程式變成一團非同步「麵條」。例如你可以照常使用 Thread.sleepInputStream.readSocket.accept——JVM 會確保不會把整個 carrier thread 都卡住。

提高可讀性與可維護性

不必再用複雜的 callback 方案或 CompletableFuture,而是撰寫線性、好理解的程式。這可減少 bug,並讓維護更容易。

不必重新造輪子

過去若要同時處理成千上萬個請求,往往得靠非同步框架、反應式程式庫(Netty、Vert.x、Project Reactor),並採用特別的程式風格。現在即使不用它們——也一樣能獲得良好的可伸縮性。

4. 架構:虛擬執行緒的「底層」如何運作

對映到 carrier threads

JVM 會建立一個小型的真正執行緒池(carrier threads)——通常和 CPU 核心數差不多。所有虛擬執行緒就像搭乘這些 carrier threads 的乘客一樣「跑」在上面。

  • 當虛擬執行緒被阻塞(例如等待網路回應)時,JVM 會把它從 carrier thread「卸下」並放入佇列。
  • 一旦該執行緒可以繼續,JVM 就把它重新安排到空閒的 carrier thread 上。

比喻:
想像你只有 4 台計程車(carrier threads),但要服務 10 000 位乘客(virtual threads)。當一位乘客抵達下車,計程車立刻去載下一位。沒有人在空等,計程車也不會被「乘客的重量」壓垮。

排程與切換

JVM 會自行決定此刻要執行哪個虛擬執行緒。若執行緒在 I/O 上被阻塞,它不會妨礙其他執行緒繼續工作。

5. 虛擬執行緒的限制與特性

不是所有「虛擬」都那麼美好

不適合長時間的重度計算: 如果你的任務會持續占用 CPU(heavy CPU‑bound),虛擬執行緒不會帶來效能提升。因為 carrier threads 的數量仍受核心數限制。

某些鎖效率不佳: 舊式同步機制(例如在具原生互斥鎖的物件上使用 synchronized)可能讓 JVM 無法「凍結」虛擬執行緒。這種情況下 carrier thread 也會跟著等待,降低可伸縮性。

並非所有程式庫都友善: 如果某個程式庫進行原生呼叫或使用特殊的鎖,虛擬執行緒的行為可能不如預期。

範例:何時不該使用 Virtual Threads

如果你的任務是在無窮迴圈中持續計算,虛擬執行緒完全不會帶來好處。最終仍受限於核心數。

Thread.ofVirtual().start(() -> {
    while (true) {
        // 一直計算到無窮
    }
});

結果:
會有一個 carrier thread 被這個虛擬執行緒占住,其他任務只能排隊等待。

6. 比較:Platform Thread vs Virtual Thread

特性 Platform Thread(傳統) Virtual Thread(虛擬)
管理者 作業系統 JVM
每個執行緒的記憶體 數個 MB 數十 KB
執行緒數量 通常 < 10_000 數千、數十萬
建立成本
可伸縮性 受限 幾乎不受限
適合用於 長時任務、CPU 密集 短暫任務、I/O 密集
執行緒切換 OS JVM
相容性 100% 幾乎總是如此,但有一些細節

7. 範例:Virtual Threads 前後對比的伺服器寫法

之前(Platform Threads)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    new Thread(() -> handleClient(client)).start();
}

問題:
大約在 5 000 個連線後,伺服器就會開始吃不消。

之後(Virtual Threads,Java 21+)

ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
    Socket client = serverSocket.accept();
    Thread.ofVirtual().start(() -> handleClient(client));
}

魔法般的效果:
現在可以處理成千上萬個連線——不再需要擔心執行緒上限!

9. 遷移到虛擬執行緒的常見錯誤

錯誤 №1:期待計算型任務獲得加速。 虛擬執行緒無法加速會把 CPU 塞滿的任務。這類任務仍然受限於核心數。

錯誤 №2:沿用舊式阻塞式同步。 如果你使用舊式鎖(例如在可能被原生方式「鎖住」的物件上使用 synchronized),虛擬執行緒可能無法從 carrier thread 上卸載,進而喪失優勢。

錯誤 №3:忽略第三方程式庫的行為。 某些第三方程式庫可能尚未準備好與虛擬執行緒合作(例如使用 JNI 或原生鎖)。

錯誤 №4:期待魔法般的效能成長。 虛擬執行緒不是萬靈丹。它們不會讓所有東西都變快;它們只是讓 I/O‑bound 任務的並行成本變得很低、寫法更自然。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION