Java併發程式設計:執行緒池ThreadPoolExecutor

耶low發表於2020-05-19

  多執行緒的程式的確能發揮多核處理器的效能。雖然與程式相比,執行緒輕量化了很多,但是其建立和關閉同樣需要花費時間。而且執行緒多了以後,也會搶佔記憶體資源。如果不對執行緒加以管理的話,是一個非常大的隱患。而執行緒池的目的就是管理執行緒。當你需要一個執行緒時,你就可以拿一個空閒執行緒去執行任務,當任務執行完後,執行緒又會歸還到執行緒池。這樣就有效的避免了重複建立、關閉執行緒和執行緒數量過多帶來的問題。

Java併發包提供的執行緒池

 

注:摘自《實戰Java高併發程式設計》

  如圖是Java併發包下提供的執行緒池功能。其中ExecutorService介面提供一些操作執行緒池的方法。而Executors相當於一個執行緒池工廠類,它裡面有幾種現成的具備某種特定功能的執行緒池工廠方法。看到這些應該不陌生,舉個我們平時最常使用的例子:

//建立一個大小為10的固定執行緒池
ExecutorService threadpool= Executors.newScheduledThreadPool(10);

  下面簡單介紹一下這些工廠方法:

  newFixedThreadPool()方法:固定執行緒數量執行緒池。傳入的數字就是執行緒的數量,如果有空閒執行緒就去執行任務,如果沒有空閒執行緒就會把任務放到一個任務佇列,等到有執行緒空閒時便去處理佇列中的任務。

  newSingleThreadExecutor()方法:只有一個執行緒的執行緒池。同樣,超出的任務會被放到任務佇列,等這個執行緒空閒時就會去按順序處理。

  newCachedThreadPool()方法:可以根據實際情況擴充的執行緒池。當沒有空閒執行緒去執行新任務時,就會再建立新的執行緒去執行任務,執行完後新建的執行緒也會返回執行緒池進行復用。

  newSingleThreadScheduledExecutor()方法:返回的是ScheduledExecutorService物件。ScheduledExecutorService是繼承於ExecutorService的,有一些擴充方法,如指定執行時間。這個執行緒池大小為1,在指定時間執行任務。關於指定時間的幾個方法:schedule()是在指定時間後執行一次任務。scheduleAtFixedRate()和方法scheduleWithFixedDelay()方法,兩者都是週期性的執行任務,但是前者是以上一次任務開始為週期起點,後者是以上一次任務結束為週期起點。具體的引數大家可以在IDE裡面檢視。

  newScheduledThreadPool()方法:和上面一個方法一樣,但是可以指定執行緒池大小,其實上面那個方法也是呼叫這個方法的,只是傳入的引數是1。

執行緒池核心類

  上面簡單的對Java併發包下執行緒池的結構和API進行簡單的介紹,下面開始深入瞭解一下執行緒池。如果大家在IDE上追蹤一下上面幾個工廠方法就會發現,其中最後都會呼叫一個方法,通過上圖其實也可以發現。那就是ThreadPoolExecutor的構造方法,工廠方法只是幫我們傳入不同的引數,從而實現不同的效果,所以如果你想更自由的控制自己的執行緒池,推薦直接使用ThreadPoolExecutor建立執行緒池。下面給出這個建構函式的引數列表:

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

  引數從上到下,作用依次為:

  1.指定執行緒池種執行緒的數量。

  2.執行緒池種最大的執行緒數量,也就是最大能擴充到多少。

  3.當執行緒數量超過corePoolSize,多餘的空閒執行緒多久會被銷燬。

  4.keepAliveTime的單位。

  5.任務佇列,當空閒執行緒不夠,也不能再新建執行緒時,新提交的任務就會被放到任務佇列種。

  6.執行緒工廠,用於建立執行緒,預設的即可。

  7.拒絕策略。當任務太多,達到最大執行緒數量、任務佇列也滿了,該如何拒絕新提交的任務。

任務佇列

  任務佇列是一個BlockingQueue介面,在ThreadPoolExecutor一共有如下幾種實現類實現了BlockingQueue介面。

  SynchronousQueue:直接提交佇列。這種佇列其實不會真正的去儲存任務,每提交一個任務就直接讓空閒執行緒執行,如果沒有空閒執行緒就去新建,當達到最大執行緒數時,就會執行拒絕策略。所以使用這種任務佇列時,一般會設定很大的maximumPoolSize,不然很容易就執行了拒絕策略。newCachedThreadPool執行緒池的corePoolSize為0,maximumPoolSize無限大,它用的就是直接提交佇列。

  ArrayBlockingQueue:有界任務佇列,其建構函式必須帶一個容量引數,表示任務佇列的大小。當執行緒數量小於corePoolSize時,有任務進來優先建立執行緒。當執行緒數等於corePoolSize時,新任務就會進入任務佇列,當任務佇列滿了,才會建立新執行緒,執行緒數達到maximumPoolSize時執行拒絕策略。

  LinkedBlockingQueue:無界任務佇列,通過它的名字也應該知道了,它是個連結串列,除非沒有空間了,不然不會出現任務佇列滿了的情況,但是非常耗費系統資源。和有界任務佇列一樣,執行緒數若小於corePoolSize,新任務進來時沒有空閒執行緒的話就會建立新執行緒,當達到corePoolSize時,就會進入任務佇列。會發現沒有maximumPoolSize什麼事,newFixedThreadPool固定大小執行緒池就是用的這個任務佇列,它的corePoolSize和maximumPoolSize相等。

  PriorityBlockingQueue:優先任務佇列,它是一個特殊的無界佇列,因為它總能保證高優先順序的任務先執行。

拒絕策略

  JDK提供了四種拒絕策略。

  AbortPolicy:直接丟擲異常,阻止系統正常工作。

  CallerRunsPolicy:如果執行緒池未關閉,則在呼叫者執行緒裡面執行被丟棄的任務,這個策略不是真正的拒絕任務。比如我們在T1執行緒中提交的任務,那麼該拒絕策略就會把多餘的任務放到T1執行緒執行,會影響到提交者執行緒的效能。

  DiscardOldestPolicy:該策略會丟棄一個最老的任務,也就是即將被執行的任務,然後再次嘗試提交該任務。

  DiscardPolicy:直接丟棄多餘的任務,不做任何處理,如果允許丟棄任務,這個策略是最好的。

  以上內建的拒絕策略都實現了RejectedExecutionHandler介面,所以上面的拒絕策略無法滿足你的要求,可以自定義一個:繼承RejectedExecutionHandler並實現rejectedExecution方法。

執行緒工廠

執行緒池中的執行緒是由ThreadFactory負責建立的,一般情況下預設就行,如果有一些其他的需求,比如自定義執行緒的名稱、優先順序等,我們也可以利用ThreadFactory介面來自定義自己的執行緒工廠:繼承ThreadFactory並實現newThread方法。

執行緒池的擴充

  在ThreadPoolExecutor中有三個擴充套件方法:分別會在任務執行前beforeExecute、執行完成afterExecute、執行緒池退出時執行terminated。

  這幾個方法在哪呼叫的?在ThreadPoolExecutor中有一個內部類:Worker,每個執行緒的任務其實都是由這個類裡面的run方法執行的,貼一下這個類的原始碼:

 

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    /**
     * This class will never be serialized, but we provide a
     * serialVersionUID to suppress a javac warning.
     */
    private static final long serialVersionUID = 6138294804551838833L;

    /** Thread this worker is running in.  Null if factory fails. */
    final Thread thread;
    /** Initial task to run.  Possibly null. */
    Runnable firstTask;
    /** Per-thread task counter */
    volatile long completedTasks;
    //....省略
    
    /** Delegates main run loop to outer runWorker  */
    public void run() {
        runWorker(this);
    }
    //....省略
}

 

  接著進入這個runWorker方法:

final void runWorker(Worker w) {
    //...省略
            try {
                //任務執行前
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    //任務執行完
                    afterExecute(task, thrown);
                }
            } 
    //....省略
}

  還有一個執行緒池退出時執行的方法是在何處執行的?這個方法被呼叫的地方就不止一處了,像執行緒池的shutdown方法就會呼叫

public void shutdown() {
    //....省略。這個方法裡面就會呼叫terminated
    tryTerminate();
}

  ThreadPoolExecutor中這三個方法預設是沒有任何內容的,所以我們要自定義它也很簡單,直接重寫它們就行了:

ExecutorService threadpool= new ThreadPoolExecutor(5,5,0L,TimeUnit.SECONDS,new LinkedBlockingDeque<>()){
    @Override
    protected void beforeExecute(Thread t, Runnable r) {
        //執行任務前
    }
    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        //執行任務後
    }
    @Override
    protected void terminated() {
        //執行緒退出
    }
};

 

相關文章