Java線程池分析及策略優(yōu)化

1. 概述

本文首先闡述Java線程池的運行機制、參數(shù)配置,隨后指出其存在何種“缺陷”,最后基于現(xiàn)有機制做策略優(yōu)化,定制一種更合理、有效的線程池。

2. Java線程池機制

Java可以通過Executors提供的工廠方法創(chuàng)建線程池,也可以通過ThreadPoolExecutor直接創(chuàng)建,本質上二者并無差別。
ThreadPoolExecutor最完整的構造函數(shù)如下:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
                   TimeUnit unit, BlockingQueue<Runnable> workQueue, 
                   ThreadFactory threadFactory, RejectedExecutionHandler handler)

下面逐一來看各參數(shù)含義。

2.1 corePoolSize\maximumPoolSize

  • corePoolSize:線程池中核心線程數(shù)量。
  • maximumPoolSize:線程池同時允許存在的最大線程數(shù)量。

內部處理邏輯如下:

  1. 當線程池中工作線程數(shù)小于corePoolSize,創(chuàng)建新的工作線程來執(zhí)行該任務,不管線程池中是否存在空閑線程。
  2. 如果線程池中工作線程數(shù)達到corePoolSize,新任務嘗試放入隊列,入隊成功的任務將等待工作線程空閑時調度。
    2.1 如果隊列滿并且線程數(shù)小于maximumPoolSize,創(chuàng)建新的線程執(zhí)行該任務(注意:隊列中的任務繼續(xù)排序)。
    2.2 如果隊列滿且線程數(shù)超過maximumPoolSize,拒絕該任務。

2.2 keepAliveTime

當線程池中工作線程數(shù)大于corePoolSize,并且線程空閑時間超過keepAliveTime,則這些線程將被終止。同樣,可以將這種策略應用到核心線程,通過調用allowCoreThreadTimeout來實現(xiàn)。

2.3 BlockingQueue

任務等待隊列,用于緩存暫時無法執(zhí)行的任務。分為如下三種堵塞隊列:

  • 直接遞交。如SynchronousQueue,該策略直接將任務直接交給工作線程。如果當前沒有空閑工作線程,創(chuàng)建新線程。這種策略最好是配合unbounded線程數(shù)來使用,從而避免任務被拒絕。但當任務生產速度大于消費速度,將導致線程數(shù)不斷的增加。
  • 無界隊列。如LinkedBlockingQueue,當工作的線程數(shù)達到核心線程數(shù)時,新的任務被放在隊列上。因此,永遠不會有大于corePoolSize的線程被創(chuàng)建,maximumPoolSize參數(shù)失效。這種策略比較適合所有的任務都不相互依賴,獨立執(zhí)行。但是當任務處理速度小于任務進入速度的時候會引起隊列的無限膨脹。
  • 有界隊列。如ArrayBlockingQueue,按前面描述的corePoolSize、maximumPoolSize、BlockingQueue處理邏輯處理。隊列長度和maximumPoolSize兩個值會相互影響:
  • 長隊列 + 小maximumPoolSize。會減少CPU的使用、操作系統(tǒng)資源、上下文切換的消耗,但是會降低吞吐量,如果任務被頻繁的阻塞如IO線程,系統(tǒng)其實可以調度更多的線程。
  • 短隊列 + 大maximumPoolSize。CPU更忙,但會增加線程調度的消耗.

總結一下,IO密集型可以考慮多些線程來平衡CPU的使用,CPU密集型可以考慮少些線程減少線程調度的消耗。

2.4 ThreadFactory

線程池中的工作線程通過ThreadFactory來創(chuàng)建的,如果沒有指定,默認為Executors#defaultThreadFactory。這個時候創(chuàng)建的線程將都屬于同一個線程組,擁有同樣的優(yōu)先級和daemon狀態(tài)。采用自定義的ThreadFactory,可以配置線程的名字、線程組合daemon狀態(tài)。如果調用ThreadFactory#createThread的時候失敗,將返回null,executor將不會執(zhí)行任何任務。

2.5 RejectedExecutionHandler

當新的任務無法進入等待隊列且線程數(shù)已達maximumPoolSize上線時,需要定制拒絕策略,拒絕該任務。ThreadPoolExecutor提供了如下可選策略:

  • ThreadPoolExecutor#AbortPolicy:這個策略直接拋出RejectedExecutionException異常。
  • ThreadPoolExecutor#CallerRunsPolicy:這個策略將會使用Caller線程來執(zhí)行這個任務,這是一種feedback策略,可以降低任務提交的速度。
  • ThreadPoolExecutor#DiscardPolicy:這個策略將會直接丟棄任務。
  • ThreadPoolExecutor#DiscardOldestPolicy:這個策略將會把任務隊列頭部的任務丟棄,然后重新嘗試執(zhí)行,如果還是失敗則繼續(xù)實施策略。

除了上述策略,可以通過實現(xiàn)RejectedExecutionHandler來實現(xiàn)自己的策略。

3. Java線程池缺陷

3.1 非核心線程創(chuàng)建時機

非核心線程在隊列滿時觸發(fā)創(chuàng)建,在瞬時沖高情況下,隊列被占滿,但新創(chuàng)建的線程來不及消費等待隊列中的任務,新任務被拒絕。

舉個例子,假設有一個線程池corePoolSize=1,maximumPoolSize=2,隊列長度為10。在絕大多數(shù)情況下,添加任務的速度為每5秒1條,單任務處理時間為1秒,此時線程池中只有一個核心線程。但某個時間段內,任務生產速度增加,每秒鐘任務添加速度增長為每500ms處理1條,單任務處理時間不變,持續(xù)時間為10分鐘。基于當前的策略,行為如下:

  1. 等待隊列中任務開始累積,10秒后任務隊列滿。
  2. 第11秒,創(chuàng)建一個新線程處理新任務,第11.5秒新任務到來,此時2個線程均處于busy狀態(tài),此時任務丟棄。
  3. 之后隊列始終處于滿狀態(tài),由于調度時差任務可能進一步丟失。
  4. 10分鐘后開始隊列逐步被清空。

這里舉了最簡單的場景,此場景下始終處于滿隊列狀態(tài),現(xiàn)實中瞬時沖高要比示例中的更復雜,包括任務添加頻率不固定,每個任務處理時間的隨機性等。

3.2 排隊任務調度策略

為闡述方便,本節(jié)先定義一個最簡配置線程池配置如下:

  • 核心線程數(shù):1。
  • 隊列大小:10。
  • 最大線程數(shù):2。

非核心線程在隊列滿時觸發(fā)創(chuàng)建,并執(zhí)行當前的任務,但隊列中的任務依舊處于排隊狀態(tài)。舉例說明:

  1. 當前核心線程已滿,隊列(隊列大小為10)中處于排隊的任務編號分別為任務20-29。
  2. 當任務30到來時,隊列插入失敗,創(chuàng)建新線程,此時新線程處理任務30,而非任務20。

換句話說,任務的執(zhí)行順序和任務的添加順序是不一致的,這可能導致務堵塞。想象隊列中依次添加兩個任務A、B,并且B執(zhí)行過程中需要等待任務A的執(zhí)行結果。此時,如果任務B先于任務A執(zhí)行,任務B被堵塞,線程池調度效率降低。

4. 策略優(yōu)化

4.1 非核心線程創(chuàng)建時機

“我們不希望一直等待,等待一切都來不及”。

Java線程池調度策略一直等到任務隊列滿才開始創(chuàng)建新線程,這個不是我希望看到的。我們希望的是:可以給等待隊列設置一個閾值,一旦觸及這個閾值馬上創(chuàng)建新的線程,防止任務出現(xiàn)過度堆積。

換句話說,未達到閾值時,我們認定核心線程有能力消化全部任務;超過閾值時,我們認定核心線程已經無法滿足當前的任務請求現(xiàn)狀,必須立即創(chuàng)建新的線程消化當前的任務。另外,這個閾值應當是可配置的。

4.1.1 實現(xiàn)策略

Java線程池創(chuàng)建接口如下:

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
                   TimeUnit unit, BlockingQueue<Runnable> workQueue, 
                   ThreadFactory threadFactory, RejectedExecutionHandler handler)

其中,workQueue就是前面提到的等待隊列,類型為BlockingQueue。查看Java源碼,非核心線程創(chuàng)建點邏輯如下:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();

        //未達到核心線程池,創(chuàng)建核心線程
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

        //等待隊列添加成功,double check,一般不創(chuàng)建線程。
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //等待隊列添加失敗,嘗試創(chuàng)建線程
        else if (!addWorker(command, false))
            reject(command);
    }

也就是說,當workQueue.offer失敗時,觸發(fā)創(chuàng)建新線程。現(xiàn)在需要改成達到指定閾值時添加失敗,同時觸發(fā)創(chuàng)建線程。但添加失敗的任務不是被拒絕,而是添加到附加隊列中,等待調度。

直觀上,存在兩個隊列,基本等待隊列和附加隊列,基本隊列大小為queueSize*threshold_value,附加隊列大小為queueSize*(1-threshold_value)。但實現(xiàn)上,我們不能采用雙隊列方式,因為附加隊列是基本隊列的補充,基本隊列中一旦存在空閑,附加隊列中的任務需要原子性的移到基本隊列中。通過分析BlockingQueue的全部接口,該方案不可行。

最終,我們采用LinkedBlockingQueue做為基本隊列類型,定制一個雙生產者、單消費者隊列。將生產者分為普通生產者、超限生產者兩類。

  • 普通生產者。使用BlockingQueue原生的offer等接口添加任務,可用隊列大小為queueSize*threshold_value。Java原生線程池做為普通生產者調用該組接口。
  • 超限生產者。使用自定義的additionOffer接口添加任務,可用隊列大小為queueSize*(1-threshold_value)。我們復寫的線程池框架負責執(zhí)行次動作。

線程池定制隊列代碼如下:

/**
 * 定制雙生產者、單消費者隊列。通過閾值將隊列分為普通生產者隊列和超限生產者隊列兩部分。
 * 普通生產者隊列供Java原生線程池使用
 * 
 * @param <T>
 */
public class DwThreadBlockingQueue<T> extends LinkedBlockingQueue<T> {
    private int capacity;
    private int additionalCapacity;

    //處理原則:capacity,additionalCapacity生產的時候分開生產、消費的時候統(tǒng)一消費
    public HwThreadBlockingQueue(int queueSize, float threshold) {
        super(queueSize);

        this.capacity = queueSize * threshold;
        this.additionalCapacity = queueSize - capacity;
    }

    @Override
    public boolean addAll(Collection<? extends T> c) {
        throw new UnsupportedOperationException();
    }

    //普通生產者接口
    @Override
    public boolean add(T e) {
        if (super.size() >= capacity) {
            throw new IllegalStateException();
        }
        
        //并發(fā)下不足夠精確
        return super.add(e);
    }

    //普通生產者接口
    @Override
    public boolean offer(T e) {
        //并發(fā)下不足夠精確
        if (super.size() >= capacity) {
            return false;
        }

        return super.offer(e);
    }

    //普通生產者接口
    @Override
    public boolean offer(T e, long timeout, TimeUnit unit) throws InterruptedException {
        //并發(fā)下不足夠精確
        if (super.size() >= capacity) {
            return false;
        }

        return super.offer(e, timeout, unit);
    }

    //普通生產者接口
    @Override
    public void put(T e) throws InterruptedException {
        throw new UnsupportedOperationException();
    }

    //符合LVS替換原則,弱化為BlockingQueue時為標準的單生產者、消費者隊列
    @Override
    public int remainingCapacity() {
        int remain = super.remainingCapacity() - additionalCapacity;
        return remain >= 0 ? remain : 0;
    }
    
    //超限生產者接口
    public boolean additionalOffer(T e) {
        return super.offer(e);
    }
}

前面提到,Java原生線程池做為普通生產者添加任務,但如果普通生產者添加任務失敗,需要有一個類做為超限生產者添加到超限隊列,這個類復寫自ThreadPoolExecutor。

public class DwThreadPoolExecutor extends ThreadPoolExecutor {
    public HwThreadPoolExecutor(int poolSize, int maxSize, int keepAliveTime, int queueSize, float threshold, RejectedExecutionHandler handler) {
        
        //調用基類,傳入定制的雙生產者隊列
        super(poolSize, maxSize, keepAliveTime, TimeUnit.SECONDS,
                new HwThreadBlockingQueue<Runnable>(queueSize, threshold));

        super.setRejectedExecutionHandler((runnable, executor) -> {
            //做為超限生產者,添加超限任務
            if (!queue.additionalOffer(runnable)) {
                
                //超限任務添加失敗,轉由外部處理   
                handler.rejectedExecution(runnable, executor);
            }
        });
    }
}

4.2 任務調度策略

任務調度策略上,我們希望調度是保序的。
基本想法如下:

  • 采用任務代理類,將任務綁定時機延遲到任務執(zhí)行時,而非任務添加時。
  • 增加新的任務隊列,按添加順序保存真正的執(zhí)行任務
  • 運行時,動態(tài)從新增任務隊列中獲取頭部任務,做到FIFO。

代理類實現(xiàn)如下:

import java.util.concurrent.BlockingQueue;

public class DwRunnableAgent implements Runnable{
    //保序任務隊列
    private BlockingQueue<Runnable> realTasks = null;
    
    public DwRunnableAgent(BlockingQueue<Runnable> realTasks) {
        this.realTasks = realTasks;
    }

    @Override
    public void run() {
        //運行時綁定,頭部獲取任務,保證FIFO
        Runnable runnable = realTasks.remove();
        runnable.run();
    }
}

定制線程池框架增加如下代碼:

public class DwThreadPoolExecutor extends ThreadPoolExecutor {
    //真正任務保序隊列
    private LinkedBlockingQueue<Runnable> realRunnables = new LinkedBlockingQueue<>();

    public HwThreadPoolExecutor(int poolSize, int maxSize, int keepAliveTime, int baseQueueSize,
            int additionalQueueSize, RejectedExecutionHandler handler) {
        
        ......

        // 內部消化一次
        super.setRejectedExecutionHandler((runnable, executor) -> {
            //做為超限生產者,添加超限任務
            if (!queue.additionalOffer(runnable)) {
                //移除任務,任務真正拒絕
                realRunnables.remove(runnable);

                //超限任務添加失敗,轉由外部處理   
                handler.rejectedExecution(runnable, executor);
            }
        });
    }

    @Override
    public void execute(Runnable command) {
        // 任務記錄
        realRunnables.add(command);

        // 封裝代理對象
        super.execute(new RunnableAgent(realRunnables));
    }

}

5. 總結

本人曾嘗試了解為何Java原生線程池存在上述缺陷,但大多都在闡述如何基于當前的特性開發(fā),關于機制并沒有找到合理的解釋。

本文解決的兩個缺陷是通用的、機制類的缺陷。

新增抽象層也給工程實踐上帶來了諸多可能,如任務執(zhí)行時間統(tǒng)計、死循環(huán)檢測、架構管控等等。


轉載請注明:[隨安居士]http://www.lxweimin.com/p/896b8e18501b

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容