JDK 19 Virtual Threads 虛擬線程

前言

JDK 19 支持了virtual thread(虛擬線程):JEP 425: Virtual Threads (Preview),虛擬線程是 Loom 項目中的一個重要特性。

Project Loom

Loom 是什麼?

Loom 項目的目標是提升 Java 的併發性能。Java 自誕生就提供了線程,它是一種很方便的併發結構(先不談線程間的通信問題 0_o),但是這種線程是使用操作系統內核線程實現的,並不能滿足當前的開發需求,浪費雲計算中的寶貴資源。Loom 項目引入 fiber 作爲輕量級、高效的線程,fiber 由 JVM 管理,開發者可以使用和之前線程相同的操作,且 fiber 具有更好的性能、佔用的內存也更少。

爲什麼要引入 Loom?

在二十多年前 Java 首次發佈時,Java 最重要的特性之一就是能方便地訪問線程,提供同步原語。Java 線程爲編寫併發程序提供了相對簡單的抽象。但是在現在使用 Java 編寫併發程序的一個問題是:併發的規模。我們希望併發服務的併發量越大越好,一個服務器能處理上萬個套接字。但是由於之前的 Java 線程是使用操作系統線程實現的,在單臺服務器上創建幾千個套接字都很勉強了……

開發者就必須做出選擇:要麼直接將一個併發單元建模成線程,要麼在比線程更細力度的級別上實現併發,但是需要自己編寫異步代碼。

Java 生態引入了異步 API,包括 JDK 的異步 NIO、異步 servlet 和異步第三方庫。這些新的 API 在使用中並不優雅,而且也不容易理解,出現的原因主要是 Java 的併發單元(線程)實現得不夠。僅僅是因爲 Java 線程的運行時性能不夠,就需要放棄線程,使用各種第三方的實現。

Java 線程使用內核線程實現固然有一些優點,比如所有的 native code 都是由內核線程支持的,所以線程中的 Java 代碼能夠調用 native API。但是上面提到的缺點太大了,導致難以編寫高性能的代碼。Erlang 和 Go 等語言都提供了輕量級線程,輕量級線程越來越流行。

Loom 項目的主要目標是添加一個通過 Java 運行時管理的叫 fiber 的輕量級線程結構,fiber 可以跟現有的中建立、操作系統的線程實現一起使用。fiber 的內存佔用非常小,比內核線程輕得多,fiber 之間的任務切換開銷趨近於 0。在單個 JVM 實例上就可以生成數百萬個 fiber,開發者可以直接寫同步阻塞的調用。同時開發者並不需要爲了性能/簡單性的權衡同時提供同步和異步 API。

線程並不是一個原子結構,包括 schedulercontinuation 2 個模塊。Java fiber 構建在這 2 個模塊之上。

Virtual threads

虛擬線程(virtual thread)就是輕量級線程,可以減少編寫高吞吐高併發的應用程序的工作量。

Platform thread 是什麼?

Platform thread 是操作系統線程的包裝,platform Thread 在底層的 OS 線程上運行 Java 代碼,數量受限於操作系統線程的數量。Platform thread 通常有比較大的線程堆棧和操作系統維護的其他資源,platform thread 也支持線程的本地變量。

可以使用 platform thread 運行所有類型的任務,就是有點浪費資源,可能會資源耗盡。

Virtual thread 是什麼?

和 platform thread 一樣,virtual thread 也是 java.lang.Thread 的實例。但是 virtual thread 並不與特定的操作系統線程綁定。Virtual thread 的代碼仍然在操作系統的線程上運行,但是當 virtual thread 上運行的代碼調用阻塞 I/O 時,Java 運行時將掛起這個 virtual thread 直到其恢復。與掛起的 virtual thread 相關聯的操作系統線程可以對其他 virtual thread 執行操作。

Virtual thread 和實現方式和虛擬內存的實現方式類似。爲了模擬大量內存,操作系統將一個大的虛擬地址空間映射到有限的 RAM。類似地,爲了模擬大量線程,Java 運行時將大量 virtual thread 映射到少量操作系統線程上。

與 platform thread 不同,virtual thread 的調用堆棧較淺,例如只是一個 HTTP 調用或 JDBC 查詢。儘管 virtual thread 支持線程局部變量,但是使用時要慎重,因爲單個 JVM 可能支持數百萬個 virtual thread。

Virtual thread 適合運行大部分時間被阻塞的任務(等待 I/O 操作),但並不適合 CPU 密集型操作。

Virtual thread 的好處?

Virtual thread 適合使用在高吞吐量的併發應用程序中,尤其是併發任務需要大量等待的。例如在服務器應用程序中,就需要處理很多阻塞 I/O 操作的請求。在 virtual thread 上運行的代碼並不會比 platform thread 上運行的代碼更快,virtual thread 的好處在於更高的吞吐量,而不是速度。

使用 virtual thread

ThreadThread.Builder API 都提供了創建 platform thread 和 virtual thread 的方法。java.util.concurrent.Executors也提供了創建使用 virtual thread 的任務的 ExecutorService

下面的代碼需要使用 JDK 19,可以直接在 IDEA 中下載:

使用 Thread.Builder 創建 virtual thread

Thread thread = Thread.ofVirtual().start(() -> System.out.println("Hello"));
thread.join();

下面代碼是使用 Thread.Builder 創建 2 個 virtual thread:

try {
    Thread.Builder builder =
        Thread.ofVirtual().name("worker-", 0);

    Runnable task = () -> {
        System.out.println("Thread ID: " +
            Thread.currentThread().threadId());
    };            

    // name "worker-0"
    Thread t1 = builder.start(task);   
    t1.join();
    System.out.println(t1.getName() + " terminated");

    // name "worker-1"
    Thread t2 = builder.start(task);   
    t2.join();  
    System.out.println(t2.getName() + " terminated");
    
} catch (InterruptedException e) {
    e.printStackTrace();
}

輸出大概是下面這個樣子:

Thread ID: 21
worker-0 terminated
Thread ID: 24
worker-1 terminated

使用 Executors.newVirtualThreadPerTaskExecutor() 創建 virtual thread

try (ExecutorService myExecutor =
    Executors.newVirtualThreadPerTaskExecutor()) {
    Future<?> future =
        myExecutor.submit(() -> System.out.println("Running thread"));
    future.get();
    System.out.println("Task completed");
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}   

調度 virtual thread

Java 運行時將 virtual thread 掛載到一個 platform thread 上,操作系統像往常一樣調度 platform thread。Virtual thread 可以從對應的 platform thread 上卸載(通常發生在 virtual thread 執行阻塞 I/O 操作時)。當一個 virtual thread 被卸載後,Java 運行時調度器能掛載不同的 virtual thread。

Virtual thread 也能固定到 platform thread 上,此時在阻塞操作期間也無法卸載 virtual thread。固定的情況有:

  • virtual thread 在同步塊或同步方法中(synchronized)
  • virtual thread 在運行本地方法或 外部函數

補充說明

JDK 19 使用說明

要想運行上面的代碼,需要幾個條件:

  • IDEA 需要升級到最新版(2022.2.3),因爲最新版才包含 JDK 19 的語言特性
  • 在 Project Structure 中將 Language Level 設置爲 19 (Preview)

JVM 源碼分析

通過查看 JDK 8 和 JDK 19 的 Thread 源碼,可以發現裏面的實現已經大不一樣了。對於 Thread 這塊又涉及大量的操作系統底層接口,最好能直接看 JDK 源碼。其實有一個完全不用下載,不用配置環境查看底層源碼的方法,那就是使用 Github~~~

JDK 源碼在這裏:https://github.com/openjdk/jdk,裏面有每個 JDK 版本的源碼。

我們可以直接在裏面搜索文件,響應速度還可以。

比如想查看 Thread.c 源碼,直接在網頁上就能查看(除了跳轉功能):

static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    {"isAlive0",         "()Z",        (void *)&JVM_IsThreadAlive},
    {"setPriority0",     "(I)V",       (void *)&JVM_SetThreadPriority},
    {"yield0",           "()V",        (void *)&JVM_Yield},
    {"sleep0",           "(J)V",       (void *)&JVM_Sleep},
    {"currentCarrierThread", "()" THD, (void *)&JVM_CurrentCarrierThread},
    {"currentThread",    "()" THD,     (void *)&JVM_CurrentThread},
    {"setCurrentThread", "(" THD ")V", (void *)&JVM_SetCurrentThread},
    {"interrupt0",       "()V",        (void *)&JVM_Interrupt},
    {"holdsLock",        "(" OBJ ")Z", (void *)&JVM_HoldsLock},
    {"getThreads",       "()[" THD,    (void *)&JVM_GetAllThreads},
    {"dumpThreads",      "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
    {"getStackTrace0",   "()" OBJ,     (void *)&JVM_GetStackTrace},
    {"setNativeName",    "(" STR ")V", (void *)&JVM_SetNativeThreadName},
    {"extentLocalCache",  "()[" OBJ,    (void *)&JVM_ExtentLocalCache},
    {"setExtentLocalCache", "([" OBJ ")V",(void *)&JVM_SetExtentLocalCache},
    {"getNextThreadIdOffset", "()J",     (void *)&JVM_GetNextThreadIdOffset}
};

擴展閱讀:

參考鏈接

我的公衆號

coding 筆記、讀書筆記、點滴記錄,以後的文章也會同步到公衆號(Coding Insight)中,希望大家關注_

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章