您現在的位置是:首頁 > 綜合
Java19 帶來的虛擬執行緒是怎樣玩出花提升十倍效能的
- 由 程式設計師子悠 發表于 綜合
- 2023-01-08
虛擬執行緒作用大嗎
我們都知道一個
Java
服務其實是一個程序,當這個服務遇到高併發場景的時候我們往往會考慮使用執行緒池來提高效能,
JDK
中就自帶執行緒池,關於
JDK
的執行緒池感興趣可以去看一阿粉之前的文章
聊聊面試中的 Java 執行緒池
。今天阿粉想跟大家聊的時候
Java19
中提到的虛擬執行緒
virtual threads
。
基本概念
我們都知道
Java
中的執行緒跟作業系統的核心執行緒是一對一的,
Java
執行緒的排程其實是依賴作業系統的核心執行緒的,這就導致了我們的執行緒切換和執行就需要進行上下文切換以及消耗大量的系統資源,同時我們也知道機器的資源是昂貴的並且也是有限的,我們不能也無法肆無忌憚的建立執行緒,因此執行緒往往會成為我們系統的瓶頸。
為了解決這個問題,
Java19
中提出了一種虛擬執行緒的概念,為了區別,之前的執行緒被稱為平臺執行緒。要注意虛擬執行緒並不是用來直接取代平臺執行緒的,虛擬執行緒是建議在平臺執行緒之上的,一個平臺執行緒可以對應多個虛擬執行緒,同時一個平臺執行緒還是一一對應核心執行緒,因此上面的架構就變成了如下,一個
VT
代表一個虛擬執行緒。
如果有小夥伴對
GO
語言比較熟悉的話,就會想到
Java
中的虛擬執行緒跟
GO
中的
Goroutines
是很類似的,確實是這樣,所以說語言都是相通的。
舉個栗子
這裡我們透過分別使用平臺執行緒以及虛擬執行緒來測試一個
case
看看兩者的耗時和效能是怎樣的,測試分如下幾步,我們依次來看一下。
注意下面的測試程式碼都是在 Java19 的版本中執行的
。
平臺執行緒方式
我們透過
JDK
自帶的執行緒池
Executors。newCachedThreadPool()
來建立執行緒池,並執行一定資料任務,任務的數量我們透過入參來控制,方便後續透過主函式呼叫。
public
static
void
platformThread
(
int
size)
{
long
l = System。currentTimeMillis();
try
(
var
executor = Executors。newCachedThreadPool()) {
IntStream。range(
0
, size)。forEach(i -> {
executor。submit(() -> {
Thread。sleep(Duration。ofSeconds(
1
));
//System。out。println(i);
return
i;
});
});
}
System。out。printf(
“elapsed time: %dms\n”
, System。currentTimeMillis() - l);
}
虛擬執行緒的方式
虛擬執行緒的程式碼跟上面的程式碼十分相似,程式碼如下。可以看到,在程式碼層面上跟上面唯一的區別就是
Executors。newCachedThreadPool()
這一行變成了
Executors。newVirtualThreadPerTaskExecutor()
即代表建立的虛擬執行緒。
public
static
void
virThread
(
int
size)
{
long
l = System。currentTimeMillis();
try
(
var
executor = Executors。newVirtualThreadPerTaskExecutor()) {
IntStream。range(
0
, size)。forEach(i -> {
executor。submit(() -> {
Thread。sleep(Duration。ofSeconds(
1
));
//System。out。println(i);
return
i;
});
});
}
System。out。printf(
“elapsed time: %dms\n”
, System。currentTimeMillis() - l);
}
監控執行的執行緒
上面的兩個方法都是都是建立執行緒池用來提交任務的,但是位於具體建立了多少個執行緒我們是不知道的,所以我們還需要透過下面的程式碼來監控。
public
static
void
main
(String[] args)
{
ScheduledExecutorService scheduledExecutorService = Executors。newScheduledThreadPool(
1
);
scheduledExecutorService。scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory。getThreadMXBean();
ThreadInfo[] threadInfo = threadBean。dumpAllThreads(
false
,
false
);
long
count = Arrays。stream(threadInfo)。count();
System。out。println(count +
“ os thread”
);
},
1
,
1
, TimeUnit。SECONDS);
int
size =
100000
;
// platformThread(size);
virThread(size);
}
透過另一個執行緒池開啟一個執行緒資訊監控的執行緒,每秒鐘輸出一次當前的執行執行緒數。這裡注意,如果上面的程式碼在
IDEA
中提示報錯,找不到類,如下所示,我們可以將滑鼠放上去進行修復。
也可以手動在設定中的編譯器》
Java
編譯器這裡給自己的模組增加一個編譯引數
-parameters ——add-modules java。management ——enable-preview
。
執行
上面的三段組合在一起就是一個完整的
case
,如果這個時候如果上面的程式碼都正常,在執行的時候不出意外會出現下面的錯誤,
這裡是因為當前
Java19
中的虛擬執行緒特性還處於預覽階段,不能直接使用,我們需要在啟動引數上面配置
——enable-preview
引數,才能正常測試,如下所示,不同版本的
IDEA
可能顯示的位置不一樣,但是都是配置
VM
引數,找一下就好了。
配置好了過後再次執行就可以得到如下的結果,可以看到在
siz
e 大小為 100000 的情況下,虛擬執行緒只建立了 12 個平臺執行緒,並且只在 2523 ms 就完成了整個任務。
但是當我們執行平臺執行緒的方法的時候會發現,同樣的
size
的情況下,平臺執行緒建立了好幾千個,而且還會觸發
OOM
,因為作業系統的資源已經被耗盡了,由此可見虛擬執行緒的效能要遠遠高於平臺執行緒。YYDS!
為了避免
OOM 我們
也可以將程式碼中的
Executors。newCachedThreadPool()
方法,改成
Executors。newFixedThreadPool(xxx)
,這樣雖然可以避免大量建立執行緒導致
OOM
,但是任務執行的時長就會消耗更長,阿粉這邊測試在
size
為 10000 的情況下,配置 500 個執行緒的時候,總共花費了 20276 ms,在資料量小十倍的情況下耗時卻增長十倍。效能可想而知,感興趣的小夥伴可以自己嘗試一下。