前提
之前很長一段時間關注JDK
協程庫的開發進度,但是前一段時間比較忙很少去檢視OpenJDK
官網的內容。Java
協程專案Loom
(因為專案還在開發階段,OpenJDK
給出的官網https://openjdk.java.net/projects/loom
中只有少量Loom
專案相關的資訊)已經在2018
年之前立項,目前已經發布過基於JDK17
編譯和JDK18
編譯等早期版本,筆者在下載Loom
早期版本的時候只找到JDK18
編譯的版本:
由於該JDK
版本過高,目前可以使用主流IDE
匯入Loom-JDK-18+9
進行程式碼高亮和語法提醒,暫時找不到方法進行編譯,暫時使用該JDK
執行目錄下的的javac
命令指令碼進行編譯,使用java
命令指令碼執行。
Loom專案簡單介紹
Loom - Fibers, Continuations and Tail-Calls for the JVM
Loom
專案的標題已經凸顯了引入的三大新特性:
Fibers
:幾年前看過當時的Loom
專案的測試程式碼就是使用Fiber
這個API
(現在這個API
已經被移除),意為輕量級執行緒,即協程,又稱為輕量級使用者執行緒,很神奇的是在目前的JDK
中實際上稱為Virtual Thread
(虛擬執行緒)Continuations
:直譯為"連續",實現上有點像閉包,參考不少資料,尚未準確理解其具體含義,感覺可以"粗暴"解讀為"程式接下來要執行什麼"或者"下一個要執行的程式碼塊"Tail-Calls
:尾呼叫VM
級別支援
三個新特性不詳細展開,目前只是EA
版本,還存在修改的可能性,所以也沒必要詳細展開。
Virtual Thread使用
當前版本Loom
專案中協程使用並沒有引入一個新的公開的虛擬執行緒VirtualThread
類,雖然真的存在VirtualThread
,但這個類使用default
修飾符,隱藏在java.lang
包中,並且VirtualThread
是Thread
的子類。協程的建立API
位於Thread
類中:
使用此API
建立協程如下:
public static void main(String[] args) {
Thread fiber = Thread.startVirtualThread(() -> System.out.println("Hello Fiber"));
}
從當前的原始碼可知:
VirtualThread
會通過Thread.currentThread()
獲取父執行緒的排程器,如果在main
方法執行,那麼上面程式碼中的協程例項的父執行緒就是main
執行緒- 預設的排程器為系統建立的
ForkJoinPool
例項(VirtualThread.DEFAULT_SCHEDULER
),輸入的Runnable
例項會被封裝為RunContinuation
,最終由排程器執行 - 對於
timed unpark
(正在阻塞,等待喚醒)的協程,使用系統建立的ScheduledExecutorService
例項進行喚醒 - 這個靜態工廠方法建立完協程馬上執行,返回的是協程例項
如果按照上面的Thread.startVirtualThread()
方法去建立協程,顯然無法定義協程的名稱等屬性。Loom
專案為Thread
類引入了建造者模式,比較合理地解決了這個問題:
// 建立平臺執行緒建造器,對應於Thread例項
public static Builder.OfPlatform ofPlatform() {
return new ThreadBuilders.PlatformThreadBuilder();
}
// 建立虛擬執行緒建造器,對應於VirtualThread
public static Builder.OfVirtual ofVirtual() {
return new ThreadBuilders.VirtualThreadBuilder();
}
簡單說就是:
ofPlatform()
方法用於構建Thread
例項,這裡的Platform Thread
(平臺執行緒)其實就是JDK1.0
引入的執行緒例項,普通的使用者執行緒ofVirtual()
方法用於構建VirtualThread
例項,也就是構建協程例項
這兩個建造器例項的所有Setter
方法鏈展開如下:
public static void main(String[] args) {
Thread.Builder.OfPlatform platformThreadBuilder = Thread.ofPlatform()
// 是否守護執行緒
.daemon(true)
// 執行緒組
.group(Thread.currentThread().getThreadGroup())
// 執行緒名稱
.name("thread-1")
// 執行緒名稱字首 + 起始自增數字 => prefix + start,下一個建立的執行緒名稱就是prefix + (start + 1)
// start > 0的情況下會覆蓋name屬性配置
.name("thread-", 1L)
// 是否啟用ThreadLocal
.allowSetThreadLocals(false)
// 是否啟用InheritableThreadLocal
.inheritInheritableThreadLocals(false)
// 設定優先順序
.priority(100)
// 設定執行緒棧深度
.stackSize(10)
// 設定未捕獲異常處理器
.uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
});
// thread-1
Thread firstThread = platformThreadBuilder.unstarted(() -> System.out.println("Hello Platform Thread First"));
// thread-2
Thread secondThread = platformThreadBuilder.unstarted(() -> System.out.println("Hello Platform Thread Second"));
Thread.Builder.OfVirtual virtualThreadBuilder = Thread.ofVirtual()
// 協程名稱
.name("fiber-1")
// 協程名稱字首 + 起始自增數字 => prefix + start,下一個建立的協程名稱就是prefix + (start + 1)
// start > 0的情況下會覆蓋name屬性配置
.name("fiber-", 1L)
// 是否啟用ThreadLocal
.allowSetThreadLocals(false)
// 是否啟用InheritableThreadLocal
.inheritInheritableThreadLocals(false)
// 設定排程器,Executor例項,也就是排程器是一個執行緒池,設定為NULL會使用VirtualThread.DEFAULT_SCHEDULER
.scheduler(null)
// 設定未捕獲異常處理器
.uncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
});
// fiber-1
Thread firstFiber = virtualThreadBuilder.unstarted(() -> System.out.println("Hello Platform Virtual First"));
// fiber-2
Thread secondFiber = virtualThreadBuilder.unstarted(() -> System.out.println("Hello Platform Virtual Second"));
}
這裡可以發現一點,就是建造器是可以複用的。如果想用建造器建立同一批引數設定相同的執行緒或者協程,可以設定name(String prefix, long start)
方法,定義執行緒或者協程的名稱字首和一個大於等於0
的數字,反覆呼叫Builder#unstarted(Runnable task)
方法就能批量建立執行緒或者協程,名稱就設定為prefix + start
、prefix + (start + 1)
、prefix + (start + 2)
以此類推。協程建立基本就是這麼簡單,執行的話直接呼叫start()
方法:
public class FiberSample2 {
public static void main(String[] args) throws Exception {
Thread.ofVirtual()
.name("fiber-1")
.allowSetThreadLocals(false)
.inheritInheritableThreadLocals(false)
.unstarted(() -> {
Thread fiber = Thread.currentThread();
System.out.printf("[%s,daemon:%s,virtual:%s] - Hello World\n", fiber.getName(),
fiber.isDaemon(), fiber.isVirtual());
}).start();
// 主執行緒休眠
Thread.sleep(Long.MAX_VALUE);
}
}
目前無法在主流IDE
編譯上面的類,所以只能使用該JDK
目錄下的工具編譯和執行,具體如下:
# 執行 - 當前目錄I:\J-Projects\framework-source-code\fiber-sample\src\main\java
(1)編譯:I:\Environment\Java\jdk-18-loom\bin\javac.exe I:\J-Projects\framework-source-code\fiber-sample\src\main\java\cn\throwx\fiber\sample\FiberSample2.java
(2)執行main方法:I:\Environment\Java\jdk-18-loom\bin\java.exe cn.throwx.fiber.sample.FiberSample2
這裡也看出了一點,所有的協程例項的daemon
標識預設為true
且不能修改。
小結
如果用嚐鮮的角度去使用Loom
專案,可以提前窺探JVM
開發者們是如何基於協程這個重大特性進行開發的,這對於提高學習JDK
核心程式碼的興趣有不少幫助。從目前來看,對於協程的實現Loom
專案距離RELEASE
版本估計還有不少功能需要完善,包括新增API
的穩定性,以及協程是否能夠移植到原有的JUC
類庫中使用(當前的Loom-JDK-18+9
沒有對原來的執行緒池等類庫進行修改)等問題需要解決,所以在保持關注的過程中靜心等待吧。
(e-a-20210818 c-2-d)