執行緒池原理

下一個丶奇蹟發表於2017-07-27

友情推薦:

  1. 多執行緒中斷機制
  2. 深入Thread.sleep
  3. head first Thread.join()

物件導向程式設計中,物件建立和銷燬是很費時間的,因為建立一個物件要獲取記憶體資源或者其它更多資源。在Java中更是如此,虛擬機器將試圖跟蹤每一個物件,以便能夠在物件銷燬後進行垃圾回收。所以提高服務程式效率的一個手段就是儘可能減少建立和銷燬物件的次數,特別是對一些很耗資源的物件建立和銷燬。如何利用已有物件來服務就是一個需要解決的關鍵問題,其實這就是一些”池化資源”技術產生的原因。比如大家所熟悉的資料庫連線池就是遵循這一思想而產生的,下面將介紹的執行緒池技術同樣符合這一思想。

多執行緒技術主要解決處理器單元內多個執行緒執行的問題,它可以顯著減少處理器單元的閒置時間,增加處理器單元的吞吐能力。但如果對多執行緒應用不當,會增加對單個任務的處理時間。可以舉一個簡單的例子:
假設一臺伺服器完成一項任務的時間為T

 T1 建立執行緒的時間
 T2 線上程中執行任務的時間,包括執行緒間同步所需時間
 T3 執行緒銷燬的時間

顯然T = T1+T2+T3。注意這是一個極度簡化的假設。
可以看出T1,T3是多執行緒本身附加的開銷,使用者希望減少T1,T3所用的時間,從而減少T的時間。但一些執行緒的使用者並沒有注意到這一點,所以在應用程式中頻繁的建立或銷燬執行緒,這導致T1和T3在T中佔有非常大的比例。

執行緒池技術正是關注如何縮短或調整T1,T3時間的技術,從而提高伺服器程式效能的。它把T1、T3分別安排在伺服器程式的啟動和結束的時間段或者一些空閒的時間段,這樣在伺服器程式處理客戶請求時,不會有T1、T3的開銷了,執行緒池不僅調整T1、T3產生的時間,而且它還顯著減少了建立執行緒的數目。在看一個例子:

假設一臺伺服器每天大約要處理100000個請求,並且每個請求需要一個單獨的執行緒完成,這是一個很常用的場景。線上程池中,執行緒數量一般是固定的,所以產生執行緒總數不會超過執行緒池中執行緒的數目或者上限,而如果伺服器不利用執行緒池來處理這些請求則執行緒總數為100000。一般執行緒池尺寸是遠小於100000。所以利用執行緒池的伺服器程式不會為了建立100000而在處理請求時浪費時間,從而提高效率。

執行緒池是一種多執行緒處理方法,處理過程中將任務新增到佇列,然後在建立執行緒後自動啟動這些任務。執行緒池執行緒都是後臺執行緒,每個執行緒都使用預設的堆疊大小,以預設的優先順序執行,並處於多執行緒單元中。如果某個執行緒處於空閒狀態,則執行緒池將會排程一個任務給它,如果所有執行緒都始終保持繁忙,但將任務放入到一個佇列中,則執行緒池將在一段時間後建立另一個輔助執行緒,但執行緒的數目永遠不會超過最大值。超過最大值的執行緒可以排隊,但他們要等到其他執行緒完成後才啟動。

執行緒池主要有如下幾個應用範圍:

  1. 需要大量的執行緒來完成任務,且完成任務的時間比較短,如WEB伺服器完成網頁請求這樣的任務。因為單個任務小,而任務數量巨大,比如一個熱門網站的點選次數。 但對於長時間的任務,比如一個ftp連線請求,執行緒池的優點就不明顯了。因為ftp會話時間相對於執行緒的建立時間長多了。

  2. 對效能要求苛刻的應用,比如要求伺服器迅速相應客戶請求。

  3. 接受突發性的大量請求,但不至於使伺服器因此產生大量執行緒的應用。突發性大量客戶請求,在沒有執行緒池情況下,將產生大量執行緒,雖然理論上大部分作業系統執行緒數目最大值不是問題,短時間內產生大量執行緒可能使記憶體到達極限。

下面將討論執行緒池的簡單實現,以說明執行緒技術優點及應用領域。

執行緒池的簡單實現

一般一個簡單執行緒池至少包含下列組成部分。

  1. 執行緒池管理器(ThreadPoolManager):用於建立並管理執行緒池。
  2. 工作執行緒(WorkThread): 執行緒池中執行緒。
  3. 任務介面(Task):每個任務必須實現的介面,以供工作執行緒排程任務的執行。
  4. 任務佇列:用於存放沒有處理的任務。提供一種緩衝機制。

執行緒池管理器至少有下列功能:建立執行緒池、銷燬執行緒池、新增新任務

建立執行緒池的部分程式碼如下:

public class ThreadPoolManager {
    private int threadCount; //啟動的執行緒數
    private WorkThread[] handlers; //執行緒陣列
    private ArrayList<Runnable> taskVector = new ArrayList<Runnable>(); //任務佇列

    ThreadPoolManager(int threadCount) {
       this.threadCount = threadCount;
       for (int i = 0; i < threadCount; i++) {
           handlers[i] = new WorkThread();
           handlers[i].start();
       }
    }

    void shutdown() {   
       synchronized (taskVector) {
           while (!taskVector.isEmpty())
              taskVector.remove(0); //清空任務佇列
       }
       for (int i = 0; i < threadCount; i++) {
           handlers[i] = new WorkThread();
           handlers[i].interrupt(); //結束執行緒
       }
    }

    void execute(Runnable newTask) { //增加新任務
       synchronized (taskVector) {
           taskVector.add(newTask);
           taskVector.notifyAll();
       }

    }

    private class WorkThread extends Thread {
       public void run() {
           Runnable task = null;
           for (;;) {
              synchronized (taskVector) {//獲取一個新任務
                  if (taskVector.isEmpty())
                     try {
                         taskVector.wait();
                         task = taskVector.remove(0);
                     } catch (InterruptedException e) {
                         break;
                     }
              }
              task.run();
           }
       }
    }
}

ThreadPoolManager建構函式允許使用者設定啟動的執行緒數量,並且需要的建立執行緒。Shutdown函式主要關閉開啟的執行緒和清空還沒有執行的任務,execute函式將任務加入到工作佇列,並且喚醒等待的執行緒。WorkThread就是實際的工作執行緒,工作執行緒是一個可以迴圈執行任務的執行緒,在沒有任務時將等待,當有任務時,會被喚醒。任務介面是為所有任務提供統一的介面,以便工作執行緒處理,在這裡我們採用java定義的Runnable介面,使用者可以實現這個介面來完成想要的事務。實現一個執行緒池需要了解執行緒的同步機制,這部分將在後面介紹。

Java自帶執行緒池

自從Java1.5之後,Java 提供了自己的執行緒池ThreadPoolExecutor和ScheduledThreadPoolExecutor,我們先看類之間的結構圖。

執行緒池

關於執行緒池的主要類有如下幾部分:
介面:Executor、ExecutorService、ScheduledExecutorService
類:Executors、AbstractExecutorService、ThreadPoolExecutor、ScheduledThreadPoolExecutor。

ThreadPoolExecutor

首先看看ThreadPoolExecutor的建構函式,ThreadPoolExecutor提供了幾個建構函式,我們先來引數最全建構函式的含義。

    public ThreadPoolExecutor(int corePoolSize,
                                 int maximumPoolSize,
                                 long keepAliveTime,
                                 TimeUnit unit,
                                 BlockingQueue<Runnable> workQueue,
                                 ThreadFactory threadFactory,
                               RejectedExecutionHandler handler)
  • corePoolSize:執行緒池維護執行緒的最少數量
  • maximumPoolSize:執行緒池維護執行緒的最大數量
  • keepAliveTime:執行緒池維護執行緒所允許的空閒時間 ,所以如果任務很多,並且每個任務執行的時間比較短,可以適當調大這個引數來提高執行緒的利用率。
  • unit: keepAliveTime 引數的單位,可選的單位:天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS)和納秒(NANOSECONDS)
  • workQueue:任務佇列,用來存放我們所定義的任務處理執行緒,BlockingQueue是一種帶鎖的阻塞佇列,我們將在後面專門講解這種資料結構,BlockingQueue有四種選擇:
    (1)ArrayBlockingQueue,是一種基於陣列的有界阻塞佇列,此佇列按 FIFO(先進先出)原則對元素進行操作;
    (2)LinkedBlockingQueue,是一個基於連結串列的阻塞佇列,此佇列也按FIFO (先進先出)對元素進行操作,吞吐量通常要高於ArrayBlockingQueue, Executors.newFixedThreadPool()使用了這種佇列;
    (3)SynchronousQueue;是一種不儲存元素的阻塞佇列,每個插入操作必須等另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,Executors.newCachedThreadPool使用了這個佇列;
    (4)PriorityBlockingQueue,是一種具有優先權的阻塞佇列,優先順序大的任務可以先執行,使用者由此可以控制任務的執行順序。這四種阻塞佇列都有自己的使用場景,使用者可以根據需要自己決定使用。
  • threadFactory:建立新執行緒時使用的工廠,threadFactory有兩種選擇:
    (1)DefaultThreadFactory,將建立一個同執行緒組且預設優先順序的執行緒;
    (2)PrivilegedThreadFactory,使用訪問許可權建立一個許可權控制的執行緒。ThreadPoolExecutor預設採用DefaultThreadFactory。
  • handler 由於超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理策略,handler有四個選擇:
    (1)ThreadPoolExecutor.AbortPolicy(),將丟擲RejectedExecutionException異常;
    (2)ThreadPoolExecutor.CallerRunsPolicy(),將重試新增當前的任務,重複呼叫execute()方法;
    (3)ThreadPoolExecutor.DiscardOldestPolicy(),將拋棄舊任務;
    (4)ThreadPoolExecutor.DiscardPolicy,將直接拋棄任務。ThreadPoolExecutor預設採用AbortPolicy。

一個任務通過execute(Runnable)方法被新增到執行緒池,任務必須是一個 Runnable型別的物件,任務的執行方法就是呼叫Runnable型別物件的run()方法。當一個任務通過execute(Runnable)方法欲新增到執行緒池時,會做一下幾步:

  1. 如果此時執行緒池中的數量小於corePoolSize,即使執行緒池中的執行緒都處於空閒狀態,也要建立新的執行緒來處理被新增的任務。
  2. 如果此時執行緒池中的數量大於等於corePoolSize,但是緩衝佇列 workQueue未滿,那麼任務被放入緩衝佇列。
  3. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量小於maximumPoolSize,建新的執行緒來處理新增的任務。
  4. 如果此時執行緒池中的數量大於corePoolSize,緩衝佇列workQueue滿,並且執行緒池中的數量等於maximumPoolSize,那麼通過 handler所指定的策略來處理此任務。也就是處理任務的優先順序為:核心執行緒corePoolSize、任務佇列workQueue、最大執行緒maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。
  5. 當執行緒池中的執行緒數量大於corePoolSize時,如果某執行緒空閒時間超過keepAliveTime,執行緒將被終止。這樣,執行緒池可以動態的調整池中的執行緒數。

讀者可以參考下面的原始碼,分析execute函式執行的流程:

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // 執行handler策略
        }
    }
  當數量少於corePoolSize時的主要流程:
    private boolean addIfUnderCorePoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (poolSize < corePoolSize && runState == RUNNING)
                t = addThread(firstTask); //建立新執行緒
        } finally {
            mainLock.unlock();
        }
        if (t == null)
            return false;
        t.start();
        return true;
    }
當數量大於corePoolSize,小於maximumPoolSize,且阻塞佇列不能儲存任務時,執行的主要流程:
    private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (poolSize < maximumPoolSize && runState == RUNNING)
                t = addThread(firstTask);
        } finally {
            mainLock.unlock();
        }
        if (t == null)
            return false;
        t.start();
        return true;
    }

如果想在多執行緒環境中定期執行去執行任務,或者做一些其他事情,使用者可以通過Timer來實現,但是Timer有幾種缺陷:

  1. Timer是基於絕對時間的,容易受系統時鐘的影響;
  2. Timer只新建了一個執行緒來執行所有的TimeTask,所有TimeTask可能會相關影響;
  3. Timer不會捕獲TimerTask的異常,只是簡單地停止。

這樣勢必會影響其他TimeTask的執行。JDK提供了一種定時功能的執行緒池:ScheduledThreadPoolExecutor,它繼承了ThreadPoolExecutor,並且實現了ScheduledExecutorService介面,此介面有如下幾個方法:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

建立並執行在給定延遲後啟用的一次性操作:

public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                   long delay, TimeUnit unit);

建立並執行在給定延遲後啟用的一次性操作:

    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                       long initialDelay,
                       long period,
                     TimeUnit unit);

建立並執行一個在給定初始延遲後首次啟用的定期操作,後續操作具有給定的週期;也就是經過period 後開始執行,即在 initialDelay+period 後執行,接著在 initialDelay + 2 * period 後執行,依此類推。如果任務的任何一個執行遇到異常,則後續執行都會被取消。否則,只能通過執行程式的取消或終止方法來終止該任務。如果此任務的任何一個執行要花費比其週期更長的時間,則將推遲後續執行,但不會同時執行。

    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                          long initialDelay,
                          long delay,
                        TimeUnit unit);

建立並執行一個在給定初始延遲後首次啟用的定期操作,隨後在每一次執行終止和下一次執行開始之間都存在給定的延遲。如果任務的任一執行遇到異常,就會取消後續執行。否則,只能通過執行程式的取消或終止方法來終止該任務。

ScheduledThreadPoolExecutor也提供了幾個建構函式,下面列出的是其中最簡單的一個,只有corePoolSize一個引數。ScheduledThreadPoolExecutor的建構函式僅做的一件事就是呼叫ThreadPoolExecutor的建構函式,它使用一種帶有延時標記的等待佇列DelayedWorkQueue。DelayedWorkQueue內部使用concurrent包裡的DelayQueue,DelayQueue是一個無界的BlockingQueue,用於放置延時Delayed介面的物件,物件只能在其到期時才能從佇列中取走,我們將在專門講解這種資料結構。

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS,
              new DelayedWorkQueue());
  }

要配置一個執行緒池相對比較複雜,需要了解相關的引數,尤其是對於執行緒池的原理不是很清楚的情況下,很有可能配置的執行緒池不是較優的,因此在Executors類裡面提供了一些靜態工廠,生成一些常用的執行緒池:

public static ExecutorService newSingleThreadExecutor()

建立僅有一個執行緒工作的執行緒池,相當於單執行緒序列執行所有任務。如果這個唯一的執行緒因為異常結束,那麼將建立有一個新的執行緒來替代它。此執行緒池保證所有任務的執行順序按照任務的提交順序執行。

public static ExecutorService newCachedThreadPool()

建立一個快取執行緒池,如果執行緒池的大小超過了任務所需要的執行緒,那麼就會回收部分空閒(60秒不執行任務)的執行緒,當任務數增加時,此執行緒池又動態新增新執行緒來處理任務。此執行緒池沒有對執行緒池大小做限制,執行緒池大小完全依賴於作業系統(或者說JVM)所能夠建立的最大執行緒大小。

public static ExecutorService newFixedThreadPool(int nThreads)

建立指定大小的執行緒池。每次提交一個任務就建立一個執行緒,直到執行緒數量達到執行緒池的最大值。執行緒池的大小一旦達到最大值就會保持不變,如果某個執行緒因為執行異常而結束,那麼執行緒池會補充一個新執行緒。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

類似於newCachedThreadPool,建立一個快取執行緒池,此執行緒池還支援定時以及週期性執行任務。

public static ScheduledExecutorService newSingleThreadScheduledExecutor()

類似於newSingleThreadExecutor,建立一個單執行緒的執行緒池,此執行緒池還支援定時以及週期性執行任務。

下面用兩個例子介紹執行緒池的使用方法,第一個例子會建立一個固定大小的執行緒池,第二個例子會建立基於時間執行緒池。

第一個例子

    ExecutorService pool = Executors.newFixedThreadPool(2);
       //建立四個任務
       Thread t1 = new Task1();
       Thread t2 = new Task2();
       Thread t3 = new Task3();
       Thread t4 = new Task4();
       //放入執行緒池
       pool.execute(t1);
       pool.execute(t2);
       pool.execute(t3);
       pool.execute(t4);
       pool.shutdown(); //關閉執行緒池

第二個例子:

       ExecutorService pool = Executors.newScheduledThreadPool(4);
       Thread t = new Task();
       pool.scheduleAtFixedRate(t,1, 5, TimeUnit.SECONDS);

總結:

  1. FixedThreadPool是一個典型且優秀的執行緒池,它具有執行緒池的高效率和節省建立執行緒時所耗的開銷的優點。但是線上程池空閒時,即執行緒池中沒有可執行任務時,它不會釋放工作執行緒,還會佔用一定的系統資源。
  2. CachedThreadPool的特點就是線上程池空閒時,即執行緒池中沒有可執行任務時,它會釋放工作執行緒,從而釋放工作執行緒所佔用的資源。但是當出現新任務時,又要建立新的工作執行緒,這會帶來一定的系統開銷。並且在使用CachedThreadPool時,一定要注意控制任務的數量,否則大量執行緒同時執行,可能會造成系統癱瘓。

微信掃我^_^

這裡寫圖片描述

相關文章