利用虛擬線程重寫自定義異步功能

最近在使用JDK 21的虛擬線程功能,感覺對於性能測試來說,還是非常值得推廣的。通過之前文章介紹,相比各位也有所瞭解了,這裏跳過Java虛擬線程的介紹了。

在官方文檔中,虛擬線程其中一個適用場景就是處理多個小異步任務時,本着隨用隨創建,用完即銷燬的理念,不要進行過的的多線程管理和多線程同步設計。

這一點說完是否有些似曾相識,跟Golang應用關鍵字 go 非常一致,可以說一模一樣了。我感覺這個非常適合處理異步任務,所以對原來的自定義異步關鍵字進行了新版本的開發。舊版本的功能也是根據 go 關鍵字功能進行開發的。

方案設計

下面分享方案設計的要點

  1. 沒有采用無限創建虛擬線程的方式,還是用了一個最大並行虛擬線程數量限制
  2. 使用任務隊列設計,使用了線程安全隊列,存儲待執行的任務
  3. 設計了同款daemon線程,功能與上篇自定義異步文章類似,功能從任務隊列中獲取並執行任務
  4. 在通用的工具類中自定義關鍵字方法,功能向任務隊列中添加任務

代碼實現

任務隊列

    /**
     * 待執行任務隊列,最大容量爲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() 方法,用於執行任務。這些方法主要負責啓動線程執行任務,並且對執行任務的計數進行增減操作。

  1. execute(Runnable runnable) 方法:接受一個 Runnable 參數,該方法會在內部調用 daemon() 方法,確保守護線程已經啓動。然後,使用 Thread.startVirtualThread 啓動一個虛擬線程,對 index 進行增減操作,並執行傳入的 runnable.run()

  2. execute(Closure closure) 方法:接受一個閉包(Closure)作爲參數。與前一個方法類似,它也會調用 daemon() 方法以確保守護線程已經啓動。然後,使用 Thread.startVirtualThread 啓動一個虛擬線程,對 index 進行增減操作,並執行傳入的 closure()

這兩個方法的共同點是它們都啓動了一個虛擬線程(Virtual Thread),在這些線程中執行了傳入的任務(runnableclosure),同時通過 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() 的方法,它涉及了一些多線程處理和任務執行控制的邏輯。

  1. AtomicBoolean DaemonState = new AtomicBoolean(false):創建了一個名爲 DaemonStateAtomicBoolean 類型的變量,用於控制 daemon() 方法是否執行的狀態。

  2. static int MAX_THREAD = 10:定義了一個整數常量 MAX_THREAD,表示最大併發執行任務數量。

  3. 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虛擬線程擁有廣闊的應用前景,但就目前進展上業務服務還需要時間,但是對於性能測試來講,已經可以提前下手了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章