Java線程池原理及實現<最通俗易懂的講解>

1、線程池簡介多線程

線程池是一種多線程處理形式,處理過程當中將任務添加到隊列,而後在建立線程後自動啓動這些任務。線程池線程都是後臺線程。每一個線程都使用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。若是某個線程在託管代碼中空閒(如正在等待某個事件),則線程池將插入另外一個輔助線程來使全部處理器保持繁忙。若是全部線程池線程都始終保持繁忙,但隊列中包含掛起的工做,則線程池將在一段時間後建立另外一個輔助線程但線程的數目永遠不會超過最大值。超過最大值的線程能夠排隊,但他們要等到其餘線程完成後才啓動。異步

使用線程池減小的是線程的建立和銷燬時間,由於建立一個對象要獲取內存資源,虛擬機跟蹤每一個對象,以即可以在對象銷燬後進行垃圾回收,因此提升服務程序效率的一個重要手段就是儘量的下降建立和銷燬對象的次數,特別是一些很是耗資源的對象建立和銷燬。ide

2、組成部分函數

一、線程池管理器(ThreadPoolManager):用於建立並管理線程池。包含 建立線程池,銷燬線程池,加入新任務;性能

二、工做線程(WorkThread): 線程池中的線程只有兩種狀態:可運行狀態和等待狀態this

三、任務接口(Task):每個任務必須實現的接口<有返回值的callable和無返回值的runnable>,以供工做線程調度任務的運行。它主要規定了任務的入口。任務運行完後的收尾工做,任務的運行狀態等。spa

四、任務隊列(work queue):用於存放沒有處理的任務。提供一種緩衝機制,通常是BlockingQuene的實現類。操作系統

3、Java線程池的原理和實現線程

建立線程有兩種方式:繼承Thread或實現Runnable。3d

Thread實現了Runnable接口。提供了一個空的run()方法。因此不管是繼承Thread仍是實現Runnable,都要有本身的run()方法。

一個線程建立後就存在。調用start()方法就開始執行(執行run()方法)。調用wait進入等待或調用sleep進入休眠期,順利執行完成或休眠被中斷或執行過程當中出現異常而退出。

線程池添加任務過程:

咱們首先從線程池的建立提及,Executors.newFixedThreadPool(2)表示建立一個具備兩個線程的線程池,源代碼以下:

public class Executors {
    //生成一個最大爲nThreads的線程池執行器
  public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
}

這裏使用了LinkedBlockingQueue做爲隊列任務管理器,全部等待處理的任務都會放在該對列中,須要注意的是,此隊列是一個阻塞式的單端隊列。線程池創建好了,那就須要線程在其中運行了,線程池中的線程是在submit第一次提交任務時創建的,代碼以下:

public Future<?> submit(Runnable task) {
        //檢查任務是否爲null
        if (task == null) throw new NullPointerException();
        //把Runnable任務包裝成具備返回值的任務對象,不過此時並無執行,只是包裝
        RunnableFuture<Object> ftask = newTaskFor(task, null);
        //執行此任務
        execute(ftask);
        //返回任務預期執行結果
        return ftask;
    }

此處的代碼關鍵是execute方法,它實現了三個職責。

一、建立足夠多的工做線程數,不大於最大線程數量,並保持線程處於運行或等待狀態。

二、把等待處理的任務放到任務隊列中

三、從任務隊列中取出來執行

其中此處的關鍵是工做線程的建立,它也是經過new Thread方式建立的一個線程,只是它建立的並非咱們的任務線程(雖然咱們的任務實現了Runnable接口,但它只是起了一個標誌性的做用),而是通過包裝的Worker線程,代碼以下:  

private final class Worker implements Runnable {
// 運行一次任務
    private void runTask(Runnable task) {
        /* 這裏的task纔是咱們自定義實現Runnable接口的任務 */
        task.run();
        /* 該方法其它代碼略 */
    }
    // 工做線程也是線程,必須實現run方法
    public void run() {
        try {
            Runnable task = firstTask;
            firstTask = null;
            while (task != null || (task = getTask()) != null) {
                runTask(task);
                task = null;
            }
        } finally {
            workerDone(this);
        }
    }
    // 任務隊列中得到任務
    Runnable getTask() {
        /* 其它代碼略 */
        for (;;) {
            return r = workQueue.take();
        }
    }
}

此處爲示意代碼,刪除了大量的判斷條件和鎖資源。execute方法是經過Worker類啓動的一個工做線程,執行的是咱們的第一個任務,而後改線程經過getTask方法從任務隊列中獲取任務,以後再繼續執行,但問題是任務隊列是一個BlockingQuene,是阻塞式的,也就是說若是該隊列的元素爲0,則保持等待狀態,直到有任務進入爲止,咱們來看LinkedBlockingQuene的take方法,代碼以下:  

public E take() throws InterruptedException {
        E x;
        int c = -1;
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();
        try {
            try {
                // 若是隊列中的元素爲0,則等待
                while (count.get() == 0)
                    notEmpty.await();
            } catch (InterruptedException ie) {
                notEmpty.signal(); // propagate to a non-interrupted thread
                throw ie;
            }
            // 等待狀態結束,彈出頭元素
            x = extract();
            c = count.getAndDecrement();
            // 若是隊列數量還多於一個,喚醒其它線程
            if (c > 1)
                notEmpty.signal();
        } finally {
            takeLock.unlock();
        }
        if (c == capacity)
            signalNotFull();
        // 返回頭元素
        return x;
    }

分析到這裏,咱們就明白了線程池的建立過程:建立一個阻塞隊列以容納任務,在第一次執行任務時建立作夠多的線程(不超過許可線程數),並處理任務,以後每一個工做線程自行從任務對列中得到任務,直到任務隊列中的任務數量爲0爲止,此時,線程將處於等待狀態,一旦有任務再加入到隊列中,即召喚醒工做線程進行處理,實現線程的可複用性。

使用線程池減小的是線程的建立和銷燬時間,由於建立一個對象要獲取內存資源,虛擬機跟蹤每一個對象,以即可以在對象銷燬後進行垃圾回收,因此提升服務程序效率的一個重要手段就是儘量的下降建立和銷燬對象的次數,特別是一些很是耗資源的對象建立和銷燬。
這對於多線程應用來講很是的有幫助,好比咱們經常使用的servlet容器,每次請求處理的都是一個線程,若是不採用線程池技術,每次請求都會從新建立一個新的線程,這會致使系統的性能負荷加大,影響效率降低,主要是很low,弱爆了,這代碼誰寫的,給我出來,哈哈。

4、適時選擇不一樣的線程池來實現

Java的線程池包括ThreadPoolExecutor類和ScheduledThreadPoolExecutor類。

爲了簡化,還提供了Exceutors的靜態類,它能夠直接生成不一樣的線程池執行器,好比單線程執行器、帶緩衝功能的執行器等。

爲了理解這些執行器,咱們首先來看看ThreadPoolExecutor類,其中它複雜的構造函數能夠很好的理解線程池的做用,代碼以下:

// Java線程池的完整構造函數
public ThreadPoolExecutor(
  int corePoolSize, // 線程池長期維持的最小線程數,即便線程處於Idle狀態,也不會回收。
  int maximumPoolSize, // 線程數的上限
  long keepAliveTime, // 線程最大生命週期。
  TimeUnit unit, //時間單位                                 
  BlockingQueue<Runnable> workQueue, //任務隊列。當線程池中的線程都處於運行狀態,而此時任務數量繼續增長,則須要一個容器來容納這些任務,這就是任務隊列。
  ThreadFactory threadFactory, // 線程工廠。定義如何啓動一個線程,能夠設置線程名稱,而且能夠確認是不是後臺線程等。
  RejectedExecutionHandler handler) // 拒絕任務處理器。因爲超出線程數量和隊列容量而對繼續增長的任務進行處理的程序。

這是ThreadPoolExecutor最完整的構造函數,其餘的構造函數都是引用該構造函數實現的。

線程池的管理是這樣一個過程:首先建立線程池,而後根據任務的數量逐步將線程增大到corePoolSize數量,若是此時仍有任務增長,則放置到workQuene中,直到workQuene爆滿爲止,而後繼續增長池中的數量(加強處理能力),最終達到maximumPoolSize,那若是此時還有任務增長進來呢?這就須要handler處理了,或者丟棄任務,或者拒絕新任務,或者擠佔已有任務等。

在任務隊列和線程池都飽和的狀況下,一但有線程處於等待(任務處理完畢,沒有新任務增長)狀態的時間超過keepAliveTime,則該線程終止,也就說池中的線程數量會逐漸下降,直至爲corePoolSize數量爲止。

向線程池提交一個任務後,它的主要處理流程以下圖所示:

咱們能夠把線程池想象爲這樣一個場景:在一個生產線上,車間規定是能夠有corePoolSize數量的工人,可是生產線剛創建時,工做很少,不須要那麼多的人。隨着工做數量的增長,工人數量也逐漸增長,直至增長到corePoolSize數量爲止。此時還有任務增長怎麼辦呢?

好辦,任務排隊,corePoolSize數量的工人不停歇的處理任務,新增長的任務按照必定的規則存放在倉庫中(也就是咱們的workQuene中),一旦任務增長的速度超過了工人處理的能力,也就是說倉庫爆滿時,車間就會繼續招聘工人(也就是擴大線程數),直至工人數量到達maximumPoolSize爲止,那若是全部的maximumPoolSize工人都在處理任務時,並且倉庫也是飽和狀態,新增任務該怎麼處理呢?這就會扔一個叫handler的專門機構去處理了,它要麼丟棄這些新增的任務,要麼無視,要麼替換掉別的任務。

過了一段時間後,任務的數量逐漸減小,致使一部分工人處於待工狀態,爲了減小開支(Java是爲了減小系統的資源消耗),因而開始辭退工人,直至保持corePoolSize數量的工人爲止,此時即便沒有工做,也再也不辭退工人(池中的線程數量再也不減小),這也是保證之後再有任務時可以快速的處理。

明白了線程池的概念,咱們再來看看Executors提供的幾個線程建立線程池的便捷方法:

① newSingleThreadExecutor:單線程池。

顧名思義就是一個池中只有一個線程在運行,該線程永不超時,並且因爲是一個線程,當有多個任務須要處理時,會將它們放置到一個無界阻塞隊列中逐個處理,它的實現代碼以下:  

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,
             new LinkedBlockingQueue<Runnable()));
}

它的使用方法也很簡單,下面是簡單的示例:

public static void main(String[] args) throws ExecutionException,InterruptedException {
    // 建立單線程執行器
    ExecutorService es = Executors.newSingleThreadExecutor();
    // 執行一個任務
    Future<String> future = es.submit(new Callable<String>() {
        @Override
        public String call() throws Exception {
            return "";
        }
    });
    // 得到任務執行後的返回值
    System.out.println("返回值:" + future.get());
    // 關閉執行器
    es.shutdown();
}

② newCachedThreadPool:緩衝功能的線程。

創建了一個線程池,並且線程數量是沒有限制的(固然,不能超過Integer的最大值),新增一個任務即有一個線程處理,或者複用以前空閒的線程,或者重親啓動一個線程,可是一旦一個線程在60秒內一直處於等待狀態時(也就是一分鐘無事可作),則會被終止,其源碼以下: 

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

這裏須要說明的是,任務隊列使用了同步阻塞隊列,這意味着向隊列中加入一個元素,便可喚醒一個線程(新建立的線程或複用空閒線程來處理),這種隊列已經沒有隊列深度的概念了。

③ newFixedThreadPool:固定線程數量的線程池。

在初始化時已經決定了線程的最大數量,若任務添加的能力超出了線程的處理能力,則創建阻塞隊列容納多餘的任務,其源碼以下: 

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

上面返回的是一個ThreadPoolExecutor,它的corePoolSize和maximumPoolSize是相等的,也就是說,最大線程數量爲nThreads。若是任務增加的速度很是快,超過了LinkedBlockingQuene的最大容量(Integer的最大值),那此時會如何處理呢?會按照ThreadPoolExecutor默認的拒絕策略(默認是DiscardPolicy,直接丟棄)來處理。

以上三種線程池執行器都是ThreadPoolExecutor的簡化版,目的是幫助開發人員屏蔽過得線程細節,簡化多線程開發。當須要運行異步任務時,能夠直接經過Executors得到一個線程池,而後運行任務,不須要關注ThreadPoolExecutor的一系列參數時什麼含義。固然,有時候這三個線程不能知足要求,此時則能夠直接操做ThreadPoolExecutor來實現複雜的多線程計算。

newSingleThreadExecutor、newCachedThreadPool、newFixedThreadPool是線程池的簡化版,而ThreadPoolExecutor則是旗艦版___簡化版容易操做,須要瞭解的知識相對少些,方便使用,而旗艦版功能齊全,適用面廣,難以駕馭。

5、線程池的關閉

咱們能夠經過調用線程池的shutdown或shutdownNow方法來關閉線程池,可是它們的實現原理不一樣,shutdown的原理是隻是將線程池的狀態設置成SHUTDOWN狀態,而後中斷全部沒有正在執行任務的線程。shutdownNow的原理是遍歷線程池中的工做線程,而後逐個調用線程的interrupt方法來中斷線程,因此沒法響應中斷的任務可能永遠沒法終止。shutdownNow會首先將線程池的狀態設置成STOP,而後嘗試中止全部的正在執行或暫停任務的線程,並返回等待執行任務的列表。

只要調用了這兩個關閉方法的其中一個,isShutdown方法就會返回true。當全部的任務都已關閉後,才表示線程池關閉成功,這時調用isTerminaed方法會返回true。至於咱們應該調用哪種方法來關閉線程池,應該由提交到線程池的任務特性決定,一般調用shutdown來關閉線程池,若是任務不必定要執行完,則能夠調用shutdownNow。

6、線程池的好處

一、下降資源消耗

經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。

二、提升響應速度

三、提升線程的可管理性

線程時稀缺資源,若是無限的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配、調優和監控。

7、線程池的應用範圍

一、須要大量線程來完成任務,且完成任務的時間比較短

網頁(http)請求這種任務,使用線程池技術是很合適的。因爲單個任務小,而任務數量巨大,你可以想象一個熱門站點的點擊次數。 但對於長時間的任務,比方一個Telnet鏈接請求,線程池的長處就不明顯了。因爲Telnet會話時間比線程的建立時間大多了。

二、對性能要求苛刻的應用,比方要求server迅速響應客戶請求。

三、接受突發性的大量請求,但不至於使server所以產生大量線程的應用。突發性大量客戶請求,在沒有線程池狀況下,將產生大量線程,儘管理論上大部分操做系統線程數目最大值不是問題,短期內產生大量線程可能使內存到達極限,並出現"OutOfMemory"的錯誤。

8、總結

線程池經過線程的複用減小了線程建立和銷燬的開銷,經過使用任務隊列避免了線程的阻塞從而避免了線程調度和線程上下文切換的開銷。

相關文章
相關標籤/搜索