Java19虛擬執行緒都來了,我正在寫的執行緒程式碼會被淘汰掉嗎?

2022-10-08 06:00:34

Java19中引入了虛擬執行緒,雖然預設是關閉的,但是可以以Preview模式啟用,這絕對是一個重大的更新,今天Java架構雜談帶大家開箱驗貨,看看這傢伙實現了什麼了不起的功能。

1 為什麼需要虛擬執行緒?

小張貪小便宜,在路邊攤花一塊錢買了一籠熱氣騰騰的小籠包,下肚之後肚子疼得不行,於是在公司找坑位。掃了幾層樓,沒找到一個坑位,坑裡面的人要麼在抽菸,要麼在外放刷視訊、要麼腸道不是很順暢,蹲了半天沒拉出來。小張很鄙視在坑位裡面不幹正事的行為,此刻,與小張一同排隊等坑位的還有幾個同事...

小張突然感受到了從菊花傳來的一股無法壓制的推力,像極了JVM發生OOM前一刻的症狀。在這千鈞一髮的時刻,小張爆發了。

他把在廁所裡面抽菸刷視訊拉不出來的人全部都趕出來了,急著釋放記憶體的同事立刻進行解決了,然後趁味道還沒消散,立刻再讓出坑位把抽菸的人趕進去接著抽。

可以看到,虛擬執行緒中的阻塞操作不在阻塞正在執行的執行緒,這允許我們使用少量的載體執行緒並行處理大量的請求。

虛擬執行緒的載體執行緒是ForkJoinPool在 FIFO 模式下執行的執行緒。此池的大小預設為可用處理器的數量,可以使用系統屬性jdk.virtualThreadScheduler.parallelism 進行調整。將來,可能會有更多選項來建立自定義排程程式。

請注意:此 ForkJoinPool 不同於 [common pool](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/util/concurrent/ForkJoinPool.html #commonPool()),在並行流的實現中就使用到了common pool,此pool是在 LIFO 模式下執行的。

2.2 執行緒

執行緒是Java的基礎。當我們執行一個Java程式時,它的main方法作為第一個棧幀被呼叫。當一個方法呼叫另一個方法時,被呼叫者與呼叫者在同一個執行緒上執行,返回資訊記錄到執行緒堆疊上。方方法使用區域性變數時,它們儲存線上程堆疊的方法呼叫棧幀中。

當程式出現問題時,我們可以通過遍歷當前執行緒堆疊來進行跟蹤。

執行緒是Java程式排程的基本單位。當執行緒阻塞等待磁碟IO、網路IO或者鎖時,該執行緒被掛起,以便另一個執行緒可以在CPU上執行。構建線上程之上的例外處理、單步偵錯和分析、順序控制流和區域性變數等已經成為了編碼中使用率非常高的東西。執行緒是Java並行模型的基礎。

2.2.1 平臺執行緒

在進入虛擬執行緒的世界之前,我需要重新審視經典執行緒,我們可以將之稱為平臺執行緒

常見的Java執行緒模式實現方式有:

  • 使用核心執行緒實現;
  • 使用使用者執行緒實現;
  • 使用用執行緒+輕量級程序混合實現;

JVM規範並沒有限定Java執行緒需要那種模型,對於Windows和Linux版本使用的是1:1的執行緒,對映到輕量級程序中(每個輕量級程序都需要有一個核心執行緒支援)。

詳細閱讀:Java架構雜談的另一篇文章:一文帶你徹底理解同步和鎖的本質(乾貨)

由於大多數作業系統的實現方式決定了建立執行緒的代價比較昂貴,因此係統能夠建立的執行緒數量收到了限制,最終導致我們在程式中使用執行緒的方式產生了影響。

執行緒棧大小的限制

作業系統通常在建立執行緒時將執行緒堆疊分配為單片記憶體塊,一旦分配完成,無法調整大小。為此我們需要根據實際情況手動設定一個固定的執行緒棧大小。

如果執行緒棧被設定的過大,我們將會需要使用更多的記憶體,如果設定的太小,很容易就觸發StackOverflowException。為了避免StackOverflowException,一般的我們傾向於預留多點執行緒棧,因為消耗多點記憶體總比程式報錯要好。但這樣會限制我們在給定的記憶體量的情況下可以擁有的並行執行緒數量。

而限制可以建立的執行緒數量又會帶來並行的問題。因為伺服器應用程式一般的處理方法是每個請求分配一個執行緒( thread-per-request)。這種處理將應用程式的並行單元(任務)與平臺(執行緒)對齊,可以最大限度的簡化開發、偵錯和維護的複雜度,同時無形的帶來很多好處,如程式執行的順序錯覺(比起非同步框架,好處太明顯了)。在這種模式下,開發人員需要很少的並行意識,因為大多數請求是相互獨立的。

也許這種模式可以輕鬆的為1000個並行請求提供服務,但是卻無法支撐100萬並行的請求,即使具有足夠好的CPU和足夠大的IO頻寬。

擴充套件閱讀,如何讓伺服器支援更高的並行:網路程式設計正規化:高效能伺服器就這麼回事 | C10K,Event Loop,Reactor,Proactor

為了讓伺服器支援更大的並行請求,Java開發人員只能選擇以下幾個比較糟糕的選擇:

  • 限制程式碼的編寫方式,使其可以使用更小的堆疊大小,這迫使我們方式大多數第三方庫;
  • 投入更多的硬體,或者切換到Reactor或者Proactor程式設計風格。雖然非同步模型最近幾年有些流行,前幾年就聽說有同學的公司在專案裡面推非同步程式設計框架,但是這樣意味著我們必須以高度受限的風格進行編碼,這要求我們放棄執行緒給我們帶來的許多好處,例如斷點偵錯,堆疊跟蹤等。最終會導致犧牲了Java程式語言原本具有的一些優勢。

2.2.2 虛擬執行緒

虛擬執行緒則是一種執行緒的替代實現。在這種實現中,執行緒棧幀儲存在Java的堆記憶體中,而不是儲存在作業系統分配到單片記憶體塊中。我們再也不需要去猜測一個執行緒可能需要多少棧空間,虛擬執行緒佔用的記憶體開始時只有幾百位元組,隨著呼叫堆疊的擴充套件和收縮而自動擴充套件和收縮,這使得系統具有了更好的可伸縮性。

對於作業系統來說,仍然只知道平臺執行緒,它是基本的排程單元,虛擬執行緒是在JVM中實現的,Java執行虛擬執行緒時通過將其安裝在某個平臺執行緒(稱為載體執行緒)上來執行它。掛載虛擬執行緒到平臺執行緒的時候,JVM會將所需的執行緒棧從堆中臨時複製到載體執行緒堆疊中,並在掛載時借用載體堆疊。

當在虛擬執行緒中執行的程式碼因為IO、鎖或者其他資源可用性而阻塞時,虛擬執行緒可以從載體執行緒中解除安裝,並且複製的任何執行緒棧改動資訊都將會存回到堆中,從而釋放載體執行緒,以使其繼續執行其他虛擬執行緒。

JDK中幾乎所有的阻塞掉都已經調整過了,因此當在虛擬執行緒上遇到阻塞操作時,虛擬執行緒會從其載體上解除安裝而不是阻塞。

例如,在LockSupport中,要park執行緒的時候,做了虛擬執行緒的相容處理:

在Thread的sleep方法中,也做了相容處理:

JDK中幾乎所有的阻塞點,都做了虛擬執行緒判斷,並且會解除安裝虛擬執行緒而不是阻塞它。

虛擬執行緒對編寫多執行緒程式有影響嗎?

在載體執行緒上掛載和解除安裝虛擬現在是JVM內部的處理邏輯,在Java程式碼層面是完全不可見的。Java程式碼無法觀察到當天載體的身份,也就是說,呼叫Thread.currentThtread總是返回虛擬執行緒。

載體執行緒的ThreadLocal值對已掛載的虛擬執行緒不可見,載體執行緒的執行緒棧幀不會出現在虛擬執行緒的異常或者執行緒轉儲中。

在虛擬執行緒的生命週期中,可能在許多不同的載體執行緒上執行。

如何建立虛擬執行緒

虛擬執行緒具有相對較少的新的API,建立完虛擬執行緒後,他們是普通的Thread物件,並且表現得向我們已經所瞭解的執行緒。例如,Thread.currentThread、ThreadLocal、中斷、堆疊遍歷等,在虛擬執行緒上的工作方式與在平臺執行緒上的工作方式完全相同,這意味著我們可以自信地在虛擬執行緒上執行我們現有的程式碼。

開啟了2016個系統執行緒。

記憶體使用最多達到了接近60M,CPU使用率最多超過了4%。

3.1.2 虛擬執行緒

程式碼如下:

package com.itzhai.demo.jdk19;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

public class VirtualThreadTest {

    public static void main(String[] args) throws InterruptedException {
        Thread.sleep(10000L);
        long start = System.currentTimeMillis();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            IntStream.range(0, 2_000).forEach(i -> {
                executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1));
                    return i;
                });
            });
        }
        long end = System.currentTimeMillis();
        System.out.println("總共耗時: " + (end - start));
        Thread.sleep(10000L);
    }
}

開啟了25個系統執行緒:

記憶體使用最多不到25M,CPU使用率最多1%左右。

對比之下,高下立斷。單單是看這幾個指標差距就已經很明顯了。

3.2 如何建立虛擬執行緒

你可以使用Executors.newVirtualThreadPerTaskExecutor()為每個任務建立一個新的虛擬執行緒:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

使用Thread.startVirtualThread()or Thread.ofVirtual().start(),我們也可以顯式啟動虛擬執行緒:

Thread.startVirtualThread(() -> {
  // code to run in thread
});

Thread.ofVirtual().start(() -> {
  // code to run in thread
});

4 虛擬執行緒優勢

4.1 更好的系統伸縮性

一般的伺服器程式中,會存在大量的非活動執行緒,伺服器程式花在網路、檔案、或者資料IO上的時間要比實際執行運算要多得多。如果我們在平臺執行緒中執行每個任務,大多數時候,執行緒將在IO獲取其他資源的可用性上被阻塞。而虛擬執行緒執行 IO-bound thread-per-task應用程式更好的擺脫最大執行緒數限制的瓶頸,從而提高硬體的利用率。

虛擬執行緒是我們可以即實現硬體的最佳利用率,又可以繼續與平臺協調的程式設計風格繼續編碼,而不是類似非同步程式設計框架那種與意外程式設計風格格格不入的方式重寫程式碼。不得不說,Java的虛擬執行緒真的是實現的很不錯。

虛擬執行緒擅長IO密集型任務的擴充套件性

對於一般的CPU密集型的任務,我們一般會通過fork-join框架和並行流來獲取最佳的CPU利用率,可以很輕鬆的擴充套件受CPU限制的工作負載。

而對於IO密集型的任務,虛擬執行緒則提供了對應的應對方案,為IO密集型的任務工作提供了擴充套件性優勢。

虛擬執行緒不是萬能的,與fork-join是互補的關係。

4.2 幹掉醜陋的響應式程式設計

響應式程式設計框架是定義了很多API來實現非同步處理,而Java的虛擬執行緒是直接改造了JDK,讓你可以直接使用Java原生API就可以實現響應式程式設計框架對效能提升的效果,而且不用編寫那些令人頭疼的回撥程式碼。

許多響應式框架要求開發人員折中考慮thread-per-request的程式設計模式,更多的考慮非同步IO、回撥、執行緒共用等,來實現更充分的利用硬體資源。在響應式程式設計模型中,當一個活動要執行IO時,它會啟動給一個非同步操作,該操作將在完成時呼叫回撥函數。框架將在某個執行緒上呼叫回撥函數,但不一定是啟動操作的同一執行緒。這意味著開發人員必須將它們的邏輯分解為IO交換和計算步驟,這些步驟被縫合到一個連續的工作流程中。因為請求旨在計算任務中才使用執行緒,並行請求的數量不受執行緒數量的限制。

響應式程式設計框架的這種可伸縮性需要付出巨大的代價:你程序不得不放棄開發平臺和生態系統的一些基本特性。

在thread-per-request模型中,如果你想按順序執行兩件事情,只需順序編寫即可,其他的如迴圈、條件或者try-catch塊都是可以直接使用的。但是在非同步風格中,通常無法使用程式語言為你提供的順序組合、迭代或其他功能來構建工作流,必須通過在非同步框架使用特定的API來完成模擬迴圈和條件等,這絕對不會比語言中內建的結構那樣靈活或熟悉。如果我們使用的是阻塞操作庫,而整個庫還沒有適配非同步工作方式,我們可能也沒法使用這些。總結來說,就是我們可以在響應式程式設計中獲得可延伸性,但是我們必須放棄使用部分語言特性和生態系統。

這些框架也是我們放棄類許多便捷的Java執行時特性,如堆疊跟蹤、偵錯程式和分析器等,因為請求在每個階段都可能在不同的執行緒中執行,並且服務執行緒可能交錯屬於不同請求的計算。非同步框架的並行單元是非同步管道的一個階段,與平臺的並行單元不同。

虛擬執行緒執行我們在不放棄語言特性和執行時特性的情況活動相同的吞吐量優勢,這正是虛擬執行緒令人著迷的地方。

4.3 阻塞操作將不再掛起核心執行緒

這就跟虛擬執行緒的實現有關了,JDK做了大量的改進,以確保應用程式在使用虛擬執行緒是擁有良好的體驗:

  • 新的通訊端實現:為了更好的支援虛擬執行緒,需要讓阻塞方法可被中斷,為此使用了JEP 353 (重新實現 Legacy Socket API) and JEP 373 (重新實現舊版 DatagramSocket API)替換了Socket、ServerScoket和DatagramSocket的實現。
  • 虛擬執行緒感知:JDK中幾乎所有的阻塞點,都做了虛擬執行緒判斷,並且會解除安裝虛擬執行緒而不是阻塞它;
  • 重新審視ThreadLocal:JDK中的許多ThreadLocal用法都根據執行緒使用模式的預期變化進行了修訂;
  • 重新審視鎖:當虛擬執行緒在執行synchronized塊時,無法從載體執行緒中解除安裝,這會影響系統吞吐量的可伸縮性,如果要避免這種情況,請使用ReentrantLock代替synchronized。有一些跟蹤排查方法可以使用,具體閱讀:JEP 425: Virtual Threads (Preview)#Executing virtual threads
  • 改進的執行緒轉儲:通過使用jcmd,提供了更好的執行緒轉儲,可以過濾掉虛擬執行緒、將相關的虛擬執行緒組合在一起,或者以機器可讀的方式生成轉儲,這些轉儲可以進行後處理以獲得更好的可觀察性。

4.4 虛擬執行緒會取代掉原有的執行緒嗎?

可能很多朋友都會有這個疑問。

JEP 425: Virtual Threads (Preview)[^1]中,提到了虛擬執行緒的設計目標,同時也提到了Non-Goals(非目標):

  • 刪除傳統的執行緒實現,或靜默遷移現有應用程式以使用虛擬執行緒不是目標;
  • 改變 Java 的基本並行模型不是目標;
  • 在 Java 語言或 Java 庫中提供新的資料並行結構不是目標。Stream API仍然是並行處理大型資料集的首選方式。

虛擬執行緒不替代原有的執行緒,它們是互補的。但是許多伺服器應用程式會選擇虛擬執行緒來實現更大的可延伸性。伺服器端的程式設計師們也無需多操心,等使用的框架都支援虛擬執行緒的時候,理想的情況下,只需要改動一下框架設定,就完成了虛擬執行緒的切換,也許這個時候,我們可以為開源框架的虛擬執行緒改造做點貢獻。

5 使用虛擬執行緒,請忘掉這些東西

虛擬執行緒與之前的執行緒API沒有什麼差別,為此,使用虛擬執行緒,你需要學習的東西比較少。

但是為了更好的使用虛擬執行緒,你需要忘掉以前的一些東西。

5.1 不再依賴執行緒池

Java 5 引入了java.util.concurrent包,其中包括了ExecutorService框架,通過使用ExecutorService以策略驅動的方式管理和池化執行緒池通常比直接建立執行緒要好得多。

對於平臺執行緒,我們習慣於將它們池化,並且在一些公司的開發規範中,是一種強制措施,以限制資源利用率,否則容易耗盡記憶體,並將執行緒啟動的成本分攤到多個請求上。但是也引入了其他的問題,流例如ThreadLocal汙染導致記憶體洩露。

但是在虛擬執行緒面前,池化技術反而成了反模式。因為虛擬執行緒的初始化佔用空間非常小,所以建立虛擬執行緒在時間和記憶體上都比建立平臺執行緒開銷小得多,甚至數百萬個虛擬執行緒才使用1G記憶體。如果限制執行緒本身以外的某些資源的並行度,例如資料庫連線,我們可以使用Semaphore來獲取稀缺資源的許可。

虛擬執行緒非常輕量級,即使是短期任務也可以建立虛擬執行緒,而嘗試重用或者回收他們會適得其反。虛擬執行緒的設計也考慮到了短期任務,如HTTP請求或者JDBC查詢。

注意:我們不必放棄使用ExecutorService,依舊可以通過新的工廠方法Executors::newVirtualThreadPerTaskExecutor來獲得一個ExecutorService偽每個任務建立一個新的虛擬執行緒。

5.2 請勿過渡使用ThreadLocal

有時候,使用ThreadLocal來快取分配記憶體開銷大的資源,或者為了避免重複分配常用物件,當系統有幾百個執行緒的時候,這種模式的資源使用通常不會過多,並且比起每次使用都重新分配更高效。但是當有幾百萬個執行緒時,每個執行緒只執行一個任務,但是可能分配了更多的範例,每個範例被重用的機會要小得多,最終會導致消耗更大的效能開銷。


從本文我們可以看到虛擬執行緒給我帶來的諸多好處,它允許我們編寫可讀且可維護的程式碼的同事,不會阻塞作業系統執行緒。

在常見的後端框架支援虛擬執行緒之前,我們還需要耐心的等待一段時間。到時小張急著上廁所的時候再也不用排長隊了。


我精心整理了一份Redis寶典給大家,涵蓋了Redis的方方面面,面試官懂的裡面有,面試官不懂的裡面也有,有了它,不怕面試官連環問,就怕面試官一上來就問你Redis的Redo Log是幹啥的?畢竟這種問題我也不會。

image-20211007142531823

Java架構雜談公眾號傳送Redis關鍵字獲取pdf檔案:

image-20211010220323135

本文作者: arthinking

部落格連結: https://www.itzhai.com/articles/virtual-thread-in-java19.html

Java19虛擬執行緒都來了,我正在寫的執行緒程式碼會被淘汰掉嗎?

版權宣告: 版權歸作者所有,未經許可不得轉載,侵權必究!聯絡作者請加公眾號。

Refrences

[1]: JEP 425: Virtual Threads (Preview). Retrieved from https://openjdk.org/jeps/425
[2]: Virtual Threads: New Foundations for High-Scale Java Applications. Retrieved from https://www.infoq.com/articles/java-virtual-threads/
[3]: Virtual Threads in Java (Project Loom). Retrieved from https://www.happycoders.eu/java/virtual-threads/
[4]: 綠色執行緒. Retrieved from https://zh.m.wikipedia.org/zh-hans/%E7%BB%BF%E8%89%B2%E7%BA%BF%E7%A8%8B
[5]: Coming to Java 19: Virtual threads and platform threads. Retrieved from https://blogs.oracle.com/javamagazine/post/java-loom-virtual-threads-platform-threads
[6]: Difference Between Thread and Virtual Thread in Java. Retrieved from https://www.baeldung.com/java-virtual-thread-vs-thread