最近在使用JDK 21的虛擬線程功能,感覺對於性能測試來說,還是非常值得推廣的。通過之前文章介紹,相比各位也有所瞭解了,這裏跳過Java虛擬線程的介紹了。
在官方文檔中,虛擬線程其中一個適用場景就是處理多個小異步任務時,本着隨用隨創建,用完即銷燬的理念,不要進行過的的多線程管理和多線程同步設計。
這一點說完是否有些似曾相識,跟Golang應用關鍵字 go
非常一致,可以說一模一樣了。我感覺這個非常適合處理異步任務,所以對原來的自定義異步關鍵字進行了新版本的開發。舊版本的功能也是根據 go
關鍵字功能進行開發的。
方案設計
下面分享方案設計的要點
- 沒有采用無限創建虛擬線程的方式,還是用了一個最大並行虛擬線程數量限制
- 使用任務隊列設計,使用了線程安全隊列,存儲待執行的任務
- 設計了同款daemon線程,功能與上篇自定義異步文章類似,功能從任務隊列中獲取並執行任務
- 在通用的工具類中自定義關鍵字方法,功能向任務隊列中添加任務
代碼實現
任務隊列
/**
* 待執行任務隊列,最大容量爲MAX_WAIT_TASK
*/
static LinkedBlockingQueue<Closure> queue = new LinkedBlockingQueue(MAX_WAIT_TASK)
這段代碼是在Java中創建了一個靜態的待執行任務隊列,使用了 LinkedBlockingQueue
類型,並命名爲 queue
。在創建隊列時,使用了 MAX_WAIT_TASK
常量來指定隊列的最大容量。
根據代碼片段提供的信息,這個隊列 queue
的元素類型是 Closure
,這可能是一個自定義類型或者來自某個框架或庫的特定類。LinkedBlockingQueue
是 Java 中的一個線程安全的隊列實現,它使用鏈表實現了一個阻塞隊列,在隊列已滿或爲空時,會對添加或獲取元素的操作進行阻塞,直到條件滿足。
這段代碼創建了一個具有最大容量爲 MAX_WAIT_TASK
的阻塞隊列,用於存儲待執行的任務(Closure
類型的任務)。隊列的容量限制可以確保隊列不會無限增長,防止內存溢出或其他資源問題。當往隊列中添加元素時,如果隊列已滿,則添加操作會被阻塞,直到有空間可用。
添加任務方法:
/**
* 添加任務
* @param closure
* @return
*/
static def add(Closure closure) {
queue.add(closure)
}
執行方法
這裏寫了兩個方法,一個執行 java.lang.Runnable
,另外一個執行 groovy.lang.Closure
。
/**
* 執行任務
* @param runnable
* @return
*/
static def execute(Runnable runnable) {
daemon()
Thread.startVirtualThread {
index.getAndIncrement()
SourceCode.noError {
runnable.run()
}
index.getAndDecrement()
}
}
/**
* 執行任務
* @param closure 任務閉包
* @return
*/
static def execute(Closure closure) {
daemon()
Thread.startVirtualThread {
index.getAndIncrement()
SourceCode.noError {
closure()
}
index.getAndDecrement()
}
}
這段代碼片段展示了兩個重載的 execute()
方法,用於執行任務。這些方法主要負責啓動線程執行任務,並且對執行任務的計數進行增減操作。
-
execute(Runnable runnable)
方法:接受一個Runnable
參數,該方法會在內部調用daemon()
方法,確保守護線程已經啓動。然後,使用Thread.startVirtualThread
啓動一個虛擬線程,對index
進行增減操作,並執行傳入的runnable.run()
。 -
execute(Closure closure)
方法:接受一個閉包(Closure)作爲參數。與前一個方法類似,它也會調用daemon()
方法以確保守護線程已經啓動。然後,使用Thread.startVirtualThread
啓動一個虛擬線程,對index
進行增減操作,並執行傳入的closure()
。
這兩個方法的共同點是它們都啓動了一個虛擬線程(Virtual Thread),在這些線程中執行了傳入的任務(runnable
或 closure
),同時通過 index.getAndIncrement()
和 index.getAndDecrement()
對執行任務的計數進行了管理。
daemon線程
/**
* daemon線程狀態,保障只執行一次
* @param closure
* @return
*/
static AtomicBoolean DaemonState = new AtomicBoolean(false)
/**
* 最大併發執行任務數量
*/
static int MAX_THREAD = 10
/**
* 執行daemon線程,保障main方法結束後關閉線程池
* @return
*/
static def daemon() {
def set = DaemonState.getAndSet(true)
if (set) return
new Thread(new Runnable() {
@Override
void run() {
SourceCode.noError {
while (ThreadPoolUtil.checkMain()) {
while (index.get() < MAX_THREAD) {
def poll = queue.poll(100, TimeUnit.MILLISECONDS)
if (poll != null) {
execute(poll)
} else {
break
}
}
sleep(0.3)
}
}
}
}, "FV").start()
}
這段代碼的功能是創建一個名爲 daemon()
的方法,它涉及了一些多線程處理和任務執行控制的邏輯。
-
AtomicBoolean DaemonState = new AtomicBoolean(false)
:創建了一個名爲DaemonState
的AtomicBoolean
類型的變量,用於控制daemon()
方法是否執行的狀態。 -
static int MAX_THREAD = 10
:定義了一個整數常量MAX_THREAD
,表示最大併發執行任務數量。 -
daemon()
方法:這是一個多線程的方法,用於執行後臺守護線程任務。這個方法通過DaemonState
的狀態控制確保只執行一次。具體實現邏輯如下:- 首先,使用
DaemonState.getAndSet(true)
方法檢查DaemonState
的狀態,如果已經爲true
,則直接返回,確保方法只執行一次。 - 然後,創建一個新的線程,該線程實現了一個
Runnable
接口,在run()
方法中執行具體的任務邏輯。 - 在
run()
方法中,通過ThreadPoolUtil.checkMain()
方法檢查主線程狀態,然後進入一個循環。在循環中,檢查當前線程池中任務執行的數量,如果小於MAX_THREAD
,則從queue
中獲取任務並執行。 queue.poll(100, TimeUnit.MILLISECONDS)
從任務隊列queue
中獲取任務,設置了超時時間爲 100 毫秒,如果獲取到任務則執行execute(poll)
方法,否則跳出內部循環。- 外部循環控制着守護線程的執行條件,使用
sleep(0.3)
控制循環的時間間隔,確保不會過於頻繁地檢查任務隊列。
- 首先,使用
這裏複用了檢查main線程的方法,沒有進行兜底執行邏輯,所以可能會因爲main線程結束過早,導致任務隊列積壓任務未被執行。我們有增加這個功能也是保持了虛擬線程非線程的思想,這一點跟 go
也保持了一致。
如果想等待的話,可以使用一下方法:
waitFor {
VirtualThreadTool.queue.size() == 0
}
總結
一個簡單的異步任務執行框架就完成了,各路大神已經測試過Java虛擬線程和Golang語言的 goroutine
性能,我就不畫蛇添足了。
虛擬線程提供了更輕量級的併發模型,能夠有效地管理大規模的併發操作,提升應用程序的性能。在性能測試階段,可以利用虛擬線程模擬併發場景,評估系統在高併發負載下的表現,檢測潛在的性能瓶頸,並進行性能優化。
Java虛擬線程擁有廣闊的應用前景,但就目前進展上業務服務還需要時間,但是對於性能測試來講,已經可以提前下手了。