高優異步任務解決雙重異步集合點阻塞問題

在性能測試的實踐當中,異步任務是離不開的。Java異步編程提高了應用程序的性能和響應性,通過避免線程阻塞提高了資源利用率,並簡化了併發編程的複雜性。改善用戶體驗,避免死鎖和線程阻塞等問題。異步編程利用CompletableFuture、Future等工具和API簡化了開發流程,提高了系統的穩定性和可靠性。

緣起

我也參照了 Go 語言的 go 關鍵字,自定義了 fun 關鍵字Java自定義異步功能實踐 。但是在使用過程中,遇到了一個略顯尷尬的問題,就是如果當一個異步任務中,又增加一個異步任務,且使用集合點設置。那麼就會阻塞線程池,導致大量任務阻塞的情況。

比如一個學校,200個班級,每個班級有一個班主任,要給30個學生髮作業,之後再報告作業分發已完成。按照之前的思路,我會包裝兩個異步且設置集合點的任務,僞代碼如下:

    static void main(String[] args) {
        200.times {
            fun {
                sleep(1.0)// 模擬業務處理
                pushHomework()// 佈置作業
            }
        }

    }

    /**
     * 佈置作業
     */
    static void pushHomework() {
        FunPhaser phaser = new FunPhaser()// 創建同步屏障
        30.times {
            fun {
                sleep(1.0)// 模擬業務處理
                output("佈置作業")
            } , phaser
        }
        phaser.await()// 等待所有作業佈置完成
    }

最終的結果就是,等於最大線程數的任務會阻塞在 pushHomework() 方法中,而 pushHomework() 方法需要完成的異步任務又全都等待在線程池的等待隊列中。

初解

一開始我的思路採取優先級策略。如果區分任務的優先級,讓有可能阻塞在等待隊列的高優任務優先執行即可。所以我先想使用 java.util.concurrent.PriorityBlockingQueue 當做 java.util.concurrent.BlockingQueue 的實現當做異步線程池的等待隊列。

但也無法解決問題,因爲依然存在阻塞的問題,只不過概率變小了而已。看來不得不使用單獨的異步線程池來實現了。

關於線程池的選擇有兩種選擇:

  1. 選擇最大線程數較小的線程池,只是作爲輔助功能,防止阻塞。在普通異步任務執行時,優先執行高優任務,利用普通線程池優先執行高優任務。
  2. 選擇最小線程數較大的線程池,大概率是緩存線程池。單獨用來執行高優任務。同時也可以利用普通的線程池執行高優任務。

關於我的選擇,也沒有選擇。根據實際的情況使用吧。高優任務的多少、需要限制的頻率等等因素。我自己的項目用的是第二種,原因是我用到高優任務的機會不多,我可以在腳本中控制高優任務的數量。

方案

首先是線程池的實現代碼:

priorityPool = createFixedPool(POOL_SIZE, "P")

創建時機放在了普通線程池中:

    /**
     * 獲取異步任務連接池
     * @return
     */
    static ThreadPoolExecutor getFunPool() {
        if (asyncPool == null) {
            synchronized (ThreadPoolUtil.class) {
                if (asyncPool == null) {
                    asyncPool = createPool(POOL_SIZE, POOL_SIZE, ALIVE_TIME, new LinkedBlockingDeque<Runnable>(Constant.MAX_WAIT_TASK), getFactory("F"))
                    daemon()
                }
                priorityPool = createFixedPool(POOL_SIZE, "P")
//                priorityPool = createPool(1, POOL_MAX, ALIVE_TIME, new LinkedBlockingQueue<Runnable>(10), getFactory("P"), new ThreadPoolExecutor.DiscardOldestPolicy())
            }
        }
        return asyncPool
    }

下面是調用執行高優的異步任務的方法:

    /**
     * 執行高優異步任務
     * @param runnable
     */
    static void executeSyncPriority(Runnable runnable) {
	    if (priorityPool == null) getFunPool()
        priorityPool.execute(runnable)
    }

還有一個調用方法,用來普通線程池優先執行高優任務:

    /**
     * 執行高優任務
     */
    static void executePriority() {
        def locked = priorityLock.compareAndSet(false, true)//如果沒有鎖,則加鎖
        if (locked) {//如果加鎖成功
            while (priorityPool.getQueue().size() > 0) {
                def poll = priorityPool.getQueue().poll()
                def queue = (LinkedBlockingDeque<Runnable>) getFunPool().getQueue()
                if (poll != null) {
                    queue.offerFirst(poll)
                }

            }
            priorityLock.set(false)//解鎖
        }
    }

這裏用到了一個原子類,當做高優之行時候的鎖 private static AtomicBoolean priorityLock = new AtomicBoolean(false) ,避免在這塊浪費過多性能。這裏沒有 try-catch-finally ,此處沒有使用,確實發生異常概率較小。

我重新修改了任務隊列的實現,用的是 java.util.concurrent.LinkedBlockingDeque ,這樣我就可以將高優任務直接插入到隊列的最前頭,可以優先執行高優任務。

對於異步關鍵字,我也進行了一些改動:

    /**
     * 使用自定義同步器{@link FunPhaser}進行多線程同步
     *
     * @param f
     * @param phaser
     * @param log
     */
    public static void fun(Closure f, FunPhaser phaser, boolean log) {
        if (phaser != null) phaser.register();
        ThreadPoolUtil.executeSync(() -> {
            try {
                ThreadPoolUtil.executePriority();
                f.call();
            } finally {
                if (phaser != null) {
                    phaser.done();
                    if (log) logger.info("async task {}", phaser.queryTaskNum());
                }
            }
        });
    }

執行高優任務的關鍵字,我也進行了同樣的封裝,只不過換了個關鍵字和線程池:

    /**
     * 提交高優任務
     *
     * @param f
     * @param phaser
     * @param log
     */
    public static void funny(Closure f, FunPhaser phaser, boolean log) {
        if (phaser != null) phaser.register();
        ThreadPoolUtil.executeSyncPriority(() -> {
            try {
                f.call();
            } finally {
                if (phaser != null) {
                    phaser.done();
                    if (log) logger.info("priority async task {}", phaser.queryTaskNum());
                }
            }
        });
    }

驗證

我們修改一下開始的腳本:

    static void main(String[] args) {
        setPoolMax(2)
        6.times {
            fun {
                sleep(1.0)// 模擬業務處理
                pushHomework()// 佈置作業
            }
        }

    }

    /**
     * 佈置作業
     */
    static void pushHomework() {
        FunPhaser phaser = new FunPhaser()// 創建同步屏障
        4.times {
            fun {
                sleep(1.0)// 模擬業務處理
                output("佈置作業")
            } , phaser
        }
        phaser.await()// 等待所有作業佈置完成
    }

執行的話,線程池的 F 線程全都全都是 TIME_WAITING 狀態。當把 pushHomework() 方法改成高優關鍵字 funny 之後問題便可迎刃而解。

控制檯輸出如下:

22:47:17:160 P-1  佈置作業
22:47:17:160 P-1  佈置作業
22:47:17:160 P-1  priority async task 3
22:47:17:160 P-1  priority async task 4
22:47:18:178 F-2  佈置作業
22:47:18:179 F-2  priority async task 3
22:47:19:183 F-2  佈置作業

可以看出,已經開始有了 F 線程執行高優任務了。

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