JDK21有沒有什麼穩定、簡單又強勢的特性?

阿里妹導讀
這篇文章主要介紹了Java虛擬執行緒的發展及其在AJDK中的實現和最佳化。
閱前宣告:本文介紹的內容基於AJDK21.0.5[1]以及以上的版本,想要用更加穩定的Java虛擬執行緒還請升級哦:-)
一、基本介紹
作為開發者,想必大家對協程/纖程的概念並不陌生。在Java的Loom專案(虛擬執行緒)出現之前,協程已經在其他程式語言中得到了廣泛應用,比如Go語言就以其內建的輕量級執行緒——goroutines而聞名。
在AJDK11/AJDK8/Dragonwell11/Dragonwell8中,我們JVM團隊則是自研了wisp特性,其實現原理與其他語言的協程類似,透過減少執行緒建立和上下文切換幫助Java應用提升效能。
Java Loom專案是由Oracle發起的一個旨在為Java平臺引入輕量級執行緒(也稱為纖程,Fiber[2])的開發專案。在Java中我們使用的名詞——虛擬執行緒、協程、纖程指代的都是這個概念,其目標是讓開發者能夠更容易地編寫出高併發的應用程式,而無需擔心傳統執行緒所帶來的資源消耗和複雜性問題。
出於與上游同步一致的目的,從JDK21開始,我們不再支援wisp,而是採用loom專案的實現來繼續最佳化、研發Java虛擬執行緒特性。目前,AJDK21.0.5包含了openjdk21中loom的所有內容,並在這基礎上進一步優化了虛擬執行緒,以減少使用者在使用虛擬執行緒時遇到死鎖問題(也稱為虛擬執行緒pin問題)的情況。
二、效能參考
穩定性:去年雙十一期間,tpp已經灰度上線,目前沒有出現問題反饋。當前版本已經在tpp大規模應用,並即將在polardb上線。
效能提升:以下提供一些實驗資料供使用者參考。
表格中以ajdk21不使用協程為baseline,對比了ajdk21協程、ajdk11以及ajdk11開啟wisp協程的情況。(csw表示context switch)
三、使用虛擬執行緒
雖然Java虛擬執行緒使用起來是很簡單的,但是對比於其他只要開一個選項的特性來說,它還是需要開發者做一點適配改動的。此前很多開發者可能會被“需要修改所有的synchronized的程式碼塊”勸退,但對使用AJDK21.0.5的開發者來說,現在已經可以從修改synchronized的工作中解脫出來啦!開發者只需要去修改應用中有關建立執行緒的部分,就可以享受到JDK21虛擬執行緒帶來的提升。
3.1. 單一執行緒轉為虛擬執行緒
// 傳統 java thread,每一個執行緒都對應於底層作業系統的一個執行緒Thread javaThread = new Thread(()->{// some tasks});// 輕量的虛擬執行緒 virtual thread,它會被底層一個執行緒攜帶執行Thread virtualThread = Thread.ofVirtual().start(()->{// some tasks});
3.2. 執行緒池轉為虛擬執行緒池
執行緒池中變成虛擬執行緒池的核心:將執行緒池的工廠轉成虛擬執行緒工廠Thread.ofVirtual().factory()
使用者可以找到自己應用程式碼中的執行緒池進行相應的修改,以下給出兩種示例。
// 1. 官方推薦的每一個任務一個虛擬執行緒的使用方式// (因為建立虛擬執行緒所需要的資源很少,虛擬執行緒不需要被快取,但是沒有限流處理可能會加劇GC(因為所有被切出去的虛擬執行緒都是GC Root)ExecutorService es = Executors.newVirtualThreadPerTaskExecutor();// 2. 傳統執行緒池改造成虛擬執行緒池// 官方並不推薦這種使用方式,但如果想要為應用限流,可以透過虛擬執行緒池的方式使用,實際應用升級也有很多采用這種方式ThreadFactory factory = Thread.ofVirtual().factory();ExecutorService executorService =new ThreadPoolExecutor(MAX_WORKER_THREADS, MAX_WORKER_THREADS,10L, TimeUnit.MINUTES,new LinkedBlockingQueue<Runnable>(), factory);

官方推薦方式的隱患

雖然openjdk官方推薦使用Executors.newVirtualThreadPerTaskExecutor(); 且明確不推薦使用虛擬執行緒池,但這裡必須要提醒使用者:過多的虛擬執行緒任務有可能會導致持續不斷地FGC,甚至OOM應用退出。這種現象後的根因是,被切出的虛擬執行緒都會在GC中視為根物件,這些虛擬執行緒引用的所有物件都必須繼續保留在堆上。
以下是一個簡單的用例以說明這種情況,感興趣的同學可以嘗試一下。(實驗中堆設定成4G大小)
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.LinkedBlockingDeque;import java.util.concurrent.ThreadFactory;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;publicclassTestFGCVT{publicstaticvoidmain(String[] args){// 1024 * 1024 * (1024 * 8) bytes -> 8G ThreadFactory factory = Thread.ofVirtual().factory();// 不限流的方式可能會造成連續不斷地FGC,甚至OOM退出 ExecutorService es = Executors.newThreadPerTaskExecutor(factory);// 限流,最多有1024*100個虛擬執行緒任務// ExecutorService es = new ThreadPoolExecutor(1024*100, 1024*100, // 1024 * 100 * 1024 * 8 = 800M// 10L, TimeUnit.MINUTES,// new LinkedBlockingDeque<Runnable>(),// factory);for(int i = 0 ; i < 1024 * 1024; i++) { System.out.println("execute: " + i); es.execute(new Task(i)); }try { es.shutdown();while(!es.awaitTermination(10, TimeUnit.SECONDS)) { System.out.println("still waiting..."); } es.close(); } catch (Exception e) { e.printStackTrace(); } }}classTaskimplementsRunnable{int num; Task(int i) { num = i; }@Overridepublicvoidrun(){ Integer[] largeInt = new Integer[1024];for(int j = 0 ; j < largeInt.length; j++) { largeInt[j] = j * 100; }try { Thread.sleep(30_000); // yield this vthread 這裡雖然執行緒被切出去了,但是持有的超大Integer陣列中的所有Integer物件都會一直存活在堆上,至少佔用1024 * 8 bytes } catch (Exception e) { e.printStackTrace(); }int sum = 0;for(int j = 0 ; j < largeInt.length; j++) { sum += largeInt[j]; } System.out.println(num + ":" + sum); }}
3.3. 重要引數介紹
虛擬執行緒本身無需開任何引數就可以使用,但是對於Java應用來說,還是需要關心關於虛擬執行緒的兩個核心引數,以控制底層實際工作執行緒數量,主要引數如下:
1.-Djdk.virtualThreadScheduler.parallelism=N這個引數是並行執行緒數的設定,虛擬機器會在大部分情況下維持的工作執行緒數量與N一致,這個引數需要使用者根據自己的機器決定,一般情況下與機器的CPU核數一致(或略小於機器CPU核數)。
2.-Djdk.virtualThreadScheduler.maxPoolSize=M這個引數是最大允許建立的執行緒數量,實際應用中可以將M設定比較大,(M>>N,比如設定成M=1024),遇到pin情況(在第4小節中會詳細介紹)後觸發的補償機制在當前執行緒數量未超過M時都會生效。在補償結束後,執行緒池會盡量恢復到N個執行緒。這個補償機制主要的目的是解決虛擬執行緒中的pin問題,這種問題遇到的機率本身非常低,且處理此情況的補償機制帶來的開銷幾乎可以忽略不計,因此使用者無需擔心該引數帶來的負面影響。
此外,-Djdk.virtualThreadScheduler.minRunnable已經開放,預設是max(parallelism / 2, 1)。其餘的引數暫時沒有開放給使用者。目前虛擬執行緒的工作執行緒池預設的corePoolSize實際與parallelism一致,keepAliveTime是30s。
3.4. 不推薦的做法
如果不使用Thread.ofVirtual().factory()而選擇自行建立虛擬執行緒工廠,需要使用者對當前的虛擬執行緒機制有較高的理解。這種工廠建立執行緒的呼叫鏈上使用到的類(<clinit>)以及物件初始化操作(<init>),需要使用者自行確保全部提前載入和初始化,否則可能會導致JVM崩潰或者應用死鎖。以下給出一個自主建立虛擬執行緒池的例子。
publicstaticvoidmain(String[] args){ ForkJoinPool.ForkJoinWorkerThreadFactory forkJoinWorkerThreadFactory = new ForkJoinPool.ForkJoinWorkerThreadFactory() {@Overridepublic ForkJoinWorkerThread newThread(ForkJoinPool pool){ returnnew CarrierThread(pool); }// newThread這個呼叫鏈上的所有類和初始化都需要使用者保證遇到pin問題前全部載入和初始化。// 使用者可以選擇在執行任務前主動觸發一次newThread。如果newThread中有選擇邏輯,使用者需要保證路徑全覆蓋。// 這裡的newThread最佳使用jdk.internal.misc.CarrierThread,否則無法在遇到pinned問題時生成多餘執行緒。// 當然,使用其餘的普通執行緒編碼上並不會出現問題。}; ForkJoinPool scheduler = new ForkJoinPool(4, forkJoinWorkerThreadFactory, (t, e) -> { }, true,4, 10, 1, pool -> true, 30, TimeUnit.SECONDS);// 4 parallelism, 4 corePoolSize, 10 maximumPoolSize, 1 minimumRunnable, 30s keepAliveTime Thread.Builder builder = virtualThreadBuilder(scheduler);// 單個任務用法和Thread.ofVirtual()一致 builder.start(()->{});// es用法 ExecutorService es = Executors.newThreadPerTaskExecutor(builder.factory());}// java.lang.ThreadBuilders$VirtualThreadBuilder(Executor) 本身被JDK中標記成只為測試使用的方法,需要反射publicstatic Thread.Builder.OfVirtual virtualThreadBuilder(Executor scheduler){try { Class<?> clazz = Class.forName("java.lang.ThreadBuilders$VirtualThreadBuilder"); Constructor<?> ctor = clazz.getDeclaredConstructor(Executor.class); ctor.setAccessible(true);return (Thread.Builder.OfVirtual) ctor.newInstance(scheduler); } catch (InvocationTargetException e) { Throwable cause = e.getCause();if (cause instanceof RuntimeException re) {throw re; }thrownew RuntimeException(e); } catch (Exception e) {thrownew RuntimeException(e); }}
上述例子只是一個示範,但是我們不推薦使用。使用者應該要理解,這種獨立建立虛擬執行緒工廠的方式下,底層工作執行緒數量是由使用者自行編碼控制的,不受到-Djdk.virtualThreadScheduler.parallelism-Djdk.virtualThreadScheduler.maxPoolSize=M的影響。因此,如果混用自行建立的虛擬執行緒工廠和預設的虛擬執行緒工廠,底層的工作執行緒數量應當是這兩種工廠的工作執行緒之和。這會影響到虛擬執行緒使用的效果。
四、jdk21虛擬執行緒pin問題以及AJDK的解決方案
在此之前使用過jdk21虛擬執行緒的開發者可能會有這種體會:應用升級到21並使用虛擬執行緒後確實變快了,但是總是遇到小機率機器“卡死”的現象,更嚴重的則是百分之百會遇到一段時間後應用不再響應。
這就是前文中提到過的虛擬執行緒pin問題。如果不想了解這個問題原因,而只想確定自己的應用是否是這種情況的,可以直接嘗試AJDK21.0.5版本進行解決。想繼續瞭解原理的同學,可以繼續閱讀此小節。
4.1. 什麼是虛擬執行緒pin問題
首先,我們在介紹虛擬執行緒pin問題前,我們先介紹一下Java虛擬執行緒提升效能的原理。
Java虛擬執行緒(Virtual Thread),Java執行緒(Carrier Thread),以及作業系統執行緒(OS Thread)是 N:1:1的關係。在虛擬執行緒排程器的配合下,Java虛擬執行緒會被排程到任意一個空閒的Carrier Thread上。當Carrier Thread遇到應該讓渡出資源的事件時(比如Thread.sleep,nio read等等),會在排程器的幫助下更換虛擬執行緒任務。由於這種切換不需要在使用者態與核心態切換,因此更加高效。此外,建立一個虛擬執行緒所需要的資源遠遠小於Java執行緒,因此使用資源的方式也更加高效。
下圖是虛擬執行緒池/執行緒池的原理示意圖。灰色代表虛擬執行緒,黑色代表Java執行緒,黃色代表暫時補償的Java執行緒/虛擬執行緒。
虛擬執行緒pin則是指,當前的Virtual Thread在遇到需要被排程出去的事件時,由於JVM內部實現的原因無法從當前的Carrier Thread移除,而是選擇一直佔用當前的Carrier Thread。也就是上圖中的灰色可執行任務一直佔用黑色執行緒,而排程器無法將之切換到其他的灰色可執行任務。虛擬執行緒pin問題的場景就是,所有的Carrier Thread都被某些Virtual Thread佔用了,從而沒有任何任務能夠繼續進行下去。
典型的虛擬執行緒pin問題主要由以下幾個原因導致:
1.遇到了類載入、初始化事件
2.遇到了synchronized程式碼塊
3.應用中呼叫了native c程式碼
這些事件導致Virtual Thread無法移除的原理其實是一致的——這些當前Carrier Thread的執行緒棧是Java Frame和C/C++ native Frame混雜的,含有C/C++ native Frame的虛擬執行緒不可移除。其不可移除的原因是當前Loom Virtual Thread的凍結、恢復設計中,其執行緒棧的地址不是原封不動的。比如原來在0x1000的棧,恢復後可能是從0x2000的地址開始,而棧中的內容和之前完全一致。C/C++程式碼中會有一些操作(比如取地址)沒有辦法保證恢復後的正確性。
為了保證程式的正確性,這些虛擬執行緒就被pin在的底層的Carrier Thread上。當所有的Carrier Thread都被這種虛擬執行緒佔用,同時沒有任何事件能被滿足從而繼續執行下去的話,Java應用就卡死了。下面我們將具體分析幾個pin的場景,並介紹AJDK21.0.5的解決方案。(AJDK21.0.5已經做的工作會用成橙色標記出來。)
4.2. synchronized導致的pin問題
只說理論還是很抽象,有沒有什麼更直觀具體的例子呢?接下來,我們將結合AJDK21.0.5對synchronized程式碼塊的解決方案以及一些小的測試用例來說明。在此前,開發者需要將這個程式碼塊修改成使用juc lock來減少被pin的情況。
關注openjdk issue的同學可能知道,最新上游中的JEP491[3]是解決了synchronized問題的。但這其實只是LOOM開發分支中的一部分程式碼,其只解決了使用LM_LIGHTWEIGHT模式下的問題,如果使用LM_LEGACY鎖模式,這個問題依舊存在。AJDK21.0.5則是包含了更多的LOOM專案中的解決方案程式碼,並額外增加了解決類載入、類初始化事件導致pin問題的臨時方案。

4.2.1 執行synchronized模組中的程式碼時無法被切換(yield失敗)—— pin when hold om

現象

當虛擬執行緒在執行synchronized中的程式碼時,遇到需要等待資源的情況時,不會將執行緒資源讓出給其他協程執行任務。
synchronized(object) {try { semaphore.acquire(); // this should yield but pinned } catch (Exception e) {// TODO }}
現在我們假設
1.能夠進行semaphore.release操作的是一個新建的協程任務
2.目前所有的執行緒資源(簡單起見,可以假設只有一個執行緒)都被上述程式碼pin住,停在第三行
那麼,程式就會完全停止執行。
實際的應用情況會比這個更復雜,會有多種pin problem組合導致應用程式無法繼續執行。

原因

概括來說,當前的JVM中加鎖其繫結的是執行緒資源,而不是協程,因此如果切換協程,其鎖的持有資訊會出現問題。
TestSynchronizedPinCase1.java
import java.util.concurrent.*;publicclassTestSynchronizedPinCase1 {publicstaticvoidmain(String[] args) throws Exception{ Object object = new Object(); Semaphore semaphore = new Semaphore(1); semaphore.acquire(); Thread t1 = Thread.ofVirtual().start(() -> { System.out.println("thread1 is reaching synchronzed, running on " + Thread.currentThread()); synchronized(object) {try{ System.out.println("thread1 is acquiring semaphore"); semaphore.acquire(); // this should yield but pinned now System.out.println("thread1 acquired"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); System.out.println("thread1 released"); } } });// ensure the t1 reaches semaphore.acquire(); System.out.println("sleep 1000ms to ensure the t1 reaches `semaphore.acquire();`"); Thread.sleep(1000); System.out.println("sleep finished"); Thread t2 = Thread.ofVirtual().start(()->{ System.out.println("thread2 is running on " + Thread.currentThread()); semaphore.release(); System.out.println("thread2 released the semaphore"); }); t1.join(); t2.join(); }}
這個小示例模擬了這種情況下的pin問題。執行用例時,請注意設定引數-Djdk.virtualThreadScheduler.parallelism=1 ,其保證了底層的Carrier Thread數量為1。(如果使用者使用最新的openjdk(包含JEP491)是可以正常執行的,但是如果額外指定LM_LEGACY模式(-XX:LockingMode=1)下會失敗,感興趣的同學可以嘗試;-))

解決方案

修改VM內部對om相關的處理,將om的owner記錄為virtual thread。鎖的所有者記錄修正後,不含有C/C++ Frame的CarrierThread就可以順利選擇新的虛擬執行緒任務切換運行了。

4.2.2 進入synchronized失敗時無法移除 —— pin when enter om

現象

當虛擬執行緒在進入synchronized時,如果獲取om失敗,會直接選擇等待,不會將執行緒資源讓出給其他協程執行任務。
synchronized(object) { // object has been locked and need to wait}
現在我們假設
1.虛擬執行緒A成功獲取object om,但是需要等待某種資源,因此pin住;
2.其餘許多虛擬執行緒在等待object om,佔用了剩餘的執行緒,因此pin住;
3.能夠釋放某種資源的虛擬執行緒任務沒有資源能夠執行,無法喚醒虛擬執行緒A;
那麼,程式就會完全停止。
這裡的某種資源可以是juc,object monitor,或者是觸發了類載入邏輯中需要獲取的鎖等等。這個問題和1.1的pin problem組合在一起就是當前synchronized關鍵字會帶來的虛擬執行緒卡死問題。

原因

進入synchronized失敗後無法yield的原因,主要是ObjectMonitor::enter其相關邏輯是vm中的C/C++所寫的邏輯,其棧是C native frame,含有這種棧的協程不能夠進行上下文切換。
TestSynchronizedPinCase2.java
import java.util.concurrent.*;publicclassTestSynchronizedPinCase2 {publicstaticvoidmain(String[] args) throws Exception{ Object object = new Object(); Semaphore semaphore = new Semaphore(1); semaphore.acquire(); Thread t1 = Thread.ofVirtual().start(() -> { System.out.println("thread1 is reaching synchronzed, running on " + Thread.currentThread()); synchronized(object) {try{ System.out.println("thread1 is holding object monitor"); semaphore.acquire(); // this should yield but pinned now System.out.println("thread1 is goinng to releasing object monitor"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); System.out.println("thread1 released"); } } System.out.println("thread1 released object monitor"); });// ensure the t1 reaches semaphore.acquire(); System.out.println("sleep 1000ms"); Thread.sleep(1000); System.out.println("sleep finished"); Thread t2 = Thread.ofVirtual().start(() -> { System.out.println("thread2 is reaching synchronzed, running on " + Thread.currentThread()); synchronized(object) { // this should yield but pinned on contended monitorenter now System.out.println("thread2 is holding object monitor"); semaphore.release(); // this should not be called before t1 released monitor System.out.println("thread2 released the semaphore"); System.out.println("thread2 is goinng to releasing object monitor"); } System.out.println("thread2 released object monitor"); });// ensure the t2 reaches synchronized(object); System.out.println("sleep 1000ms"); Thread.sleep(1000); System.out.println("sleep finished"); Thread t3 = Thread.ofVirtual().start(() -> { System.out.println("thread3 is running on " + Thread.currentThread()); semaphore.release(); System.out.println("thread3 released object monitor"); }); System.out.println("before join()"); t1.join(); t2.join(); t3.join(); System.out.println("after join()"); System.out.println("reach end"); }}
同樣,我們給出一個小示例。它模擬了4.2.1和4.2.2兩種pin問題。執行這條命令式注意設定引數-Djdk.virtualThreadScheduler.parallelism=1 這個保證了底層的Carrier Thread數量為1。

解決方案

修改VM內部對om相關的處理,將ObjectMonitor::enter相關獲取鎖的C/C++ frame移除,並在恢復上下文時重新執行獲取鎖的邏輯處理。這種方式使得vm邏輯帶來的的C/C++ frame能夠等價地被凍結和恢復,虛擬執行緒安全地被Carrier Thread切換出去。
4.3 類載入、初始化事件導致的pin問題

現象

這種情況的pin問題比較難以構造和說明。簡單而言,其在vm內部執行類載入和初始化的C/C++程式碼時遇到了需要切出的事件,但是由於這些C frame,無法讓出Carrier Thread。我們給出一個實際場景中可能遇到的棧供大家參考:
#5 ObjectMonitor::enter (this=0x5654cbc01f10, current=0x7f286c7f9c00) at src/hotspot/share/runtime/objectMonitor.cpp:329#60x00007f28cad5ca83 in ObjectSynchronizer::enter (obj=..., lock=0x7f2736d59df0, current=0x7f286c7f9c00) at src/hotspot/share/runtime/synchronizer.cpp:523#70x00007f28cad676f1 in SystemDictionary::resolve_instance_class_or_null (name=name@entry=0x7f27e8edcef8, class_loader=..., class_loader@entry=..., protection_domain=..., __the_thread__=__the_thread__@entry=0x7f286c7f9c00) at src/hotspot/share/classfile/systemDictionary.cpp:607#80x00007f28cad6931e in SystemDictionary::resolve_or_null (__the_thread__=0x7f286c7f9c00, protection_domain=..., class_loader=..., class_name=0x7f27e8edcef8) at src/hotspot/share/classfile/systemDictionary.cpp:342#9 SystemDictionary::resolve_or_fail (class_name=0x7f27e8edcef8, class_loader=..., protection_domain=..., throw_error=<optimized out>, __the_thread__=0x7f286c7f9c00) at src/hotspot/share/classfile/systemDictionary.cpp:320#100x00007f28ca4dfc33 in ConstantPool::klass_at_impl (this_cp=..., which=156, __the_thread__=0x7f286c7f9c00) at src/hotspot/share/oops/constantPool.cpp:535#110x00007f28ca74a996 in ConstantPool::klass_at (__the_thread__=0x7f286c7f9c00, which=156, this=0x7f2745941640) at src/hotspot/share/oops/constantPool.hpp:391
上面是常量池解析時的棧,其SystemDictionary::resolve_instance_class_or_null需要獲取類載入器的鎖,如果失敗就會就行park。
現在我們假設
1.虛擬執行緒A持有了類載入器CLD的om,同時因為等待某種資源pin住
2.其餘虛擬執行緒因為觸發類載入,獲取CLD om失敗而pin住
3.某種資源釋放提交的任務是虛擬執行緒執行,需要執行緒資源,只有這個任務執行後,虛擬執行緒A才能繼續
上述情況即使在解決了object monitor pinned problem後,也依舊會存在(即Loom上游最新版本也存在這個問題)。假設我們使用的是解決了om pinned problem的Loom JDK,虛擬執行緒A確實可以成功釋放底層的執行緒資源,但是這個執行緒資源緊接著很可能被第二種情況的虛擬執行緒佔用。最終第三個某種資源釋放的任務依舊沒有執行緒能夠執行。

原因

類載入中獲取鎖失敗時,其棧中含有vm內執行邏輯的c/c++ native frame,這種協程不能上下文切換。
此外,即使FJP執行緒池沒有達到上限,在當前實現中不會補償增加執行緒。

解決方案

目前的解決方案是一個work around,在鎖獲取失敗後,透過JavaCall主動觸發FJP補償執行緒的邏輯。其邏輯修改在已經解決object monitor pinned problem的情況下,理論上觸發機率較小,因此不會帶來明顯的overhead。開發者使用者請按照3.3中的重要引數介紹合理為自己的Java應用設定具體數值。
五、虛擬執行緒診斷工具
AJDK21.0.5開始增強了對虛擬執行緒的診斷工具,與openjdk的工具相比,AJDK能夠額外輸出非掛載執行中的虛擬執行緒的棧資訊。
5.1. 使用
jcmd <pid>/<application-name> ThreadAndVThread.dump
5.2. 優勢介紹
目前的openjdk上游為虛擬執行緒新增的命令jcmd <pid>/<application-name> Thread.dump_to_file -format=json testdump.json不會顯示unmounted virtual thread。然而,開發人員本身可能也很關心被切換出去的協程的棧是什麼樣的。在解決了synchronized pin問題後,有很多被切走的協程還會有鎖的持有。這個資訊對於debug來說非常重要。
5.3. 效果展示
ATP平臺也會提供分析這種格式日誌的功能:
目前的ATP分析暫時只會顯示所有鎖的持有與等待情況,不會給出是否存在死鎖的結論,這會在後續的工作中進一步最佳化。如果無法解決問題可以聯絡@佳未協同分析。
六、結語
以上就是對AJDK21.0.5中的虛擬執行緒的介紹啦,這麼穩定、簡單又強勢的特性,趕快用起來!歡迎大家使用和反饋~@佳未
參考連結:

[1]https://aliyuque.antfin.com/aone355606/gfqllg/wvurnxk1oarmoqtu

[2]https://en.wikipedia.org/wiki/Fiber_(computer_science)

[3]https://bugs.openjdk.org/browse/JDK-8338383
透過SchedulerX實現分散式任務排程
企業級應用中經常會遇到資料定時處理、檔案處理、報表生成等定時處理的任務,對於任務配置和執行率有較高要求,本文介紹如何利用SchedulerX來實施企業級定時任務的排程與管理,為定時任務提供高效、穩定和靈活的解決方案。    
點選閱讀原文檢視詳情。

相關文章