JAVA線程11 - 新特性:線程池

1、概述

1. new Thread的弊端

(1)每次new Thread新建對象性能差。
(2)線程缺少統一管理,可能無限制新建線程,相互之間競爭,及可能佔用過多系統資源致使死機或oom(內存溢出)。
(3)缺少更多功能,如定時執行、按期執行、線程中斷。

2. 線程池

線程池是指管理同一組同構工做線程的資源池,線程池是與工做隊列(Work Queue)密切相關的,其中在工做隊列中保存了全部等待執行的任務。工做線程(Worker Thread)的任務很簡單:從工做隊列中獲取一個任務,執行任務,而後返回線程池並等待下一個任務。

3. 線程池的組成

一個線程池包括如下四個基本組成部分:
(1)線程池管理器(ThreadPool):用於建立並管理線程池,包括 建立線程池,銷燬線程池,添加新任務;
(2)工做線程(PoolWorker):線程池中線程,在沒有任務時處於等待狀態,能夠循環的執行任務;
(3)任務接口(Task):每一個任務必須實現的接口,以供工做線程調度任務的執行,它主要規定了任務的入口,任務執行完後的收尾工做,任務的執行狀態等;
(4)任務隊列(taskQueue):用於存放沒有處理的任務。提供一種緩衝機制。

4. 何時適合使用線程池

在多線程應用中,若是大量的資源都耗費在建立和銷燬線程上,那麼可使用線程池。

5. 合理利用線程池帶來的好處

(1)下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
(2)提升響應速度。當任務到達時,任務能夠不須要等到線程建立就能當即執行。
(3)提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。可是要作到合理的利用線程池,必須對其原理了如指掌。

6. 線程池原理

線程池的基本思想仍是一種對象池的思想,開闢一塊內存空間,裏面存放了衆多(未死亡)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成後線程對象歸池,這樣能夠避免反覆建立線程對象所帶來的性能開銷,節省了系統的資源。

2、JAVA線程池

Java經過Executors提供四種線程池,分別爲:

1. newCachedThreadPool

建立一個可緩存線程池,若是線程池長度超過處理須要,可靈活回收空閒線程;如有了新任務但線程池無空閒線程,則新建線程。示例代碼以下:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(index);
        }
    });
}
//關閉線程池 
cachedThreadPool.shutdown();
線程池爲無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的線程,而不用每次新建線程。

2. newFixedThreadPool

建立一個固定大小的線程池,可控制線程最大併發數,超出的線程會在隊列中等待。示例代碼以下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}
//關閉線程池 
fixedThreadPool.shutdown();
由於線程池大小爲3,每一個任務輸出index後sleep 2秒,因此每兩秒打印3個數字。
定長線程池的大小最好根據系統資源進行設置。如Runtime.getRuntime().availableProcessors()。可參考PreloadDataCache。

3. newSingleThreadExecutor

建立一個單線程化的線程池,它只會用惟一的工做線程來執行任務,保證全部任務按照指定順序(FIFO, LIFO, 優先級)執行。示例代碼以下:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}
//關閉線程池 
singleThreadExecutor.shutdown();
結果依次輸出,至關於順序執行各個任務。

問:如何實現線程死掉後從新啓動?
答:可使用單線程池newSingleThreadExecutor。在線程死掉後,能夠當即建立一個線程做爲替補。

4. newScheduledThreadPool

建立一個調度線程池,支持定時及週期性任務執行。延遲執行示例代碼以下:
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);
//表示延遲3秒執行。
 
//按期執行示例代碼以下:
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);
//表示延遲1秒後每3秒執行一次。

//關閉線程池 
scheduledThreadPool.shutdown();
ScheduledExecutorService比Timer更安全,功能更強大。
ScheduledExecutorService不能定義某個時間點執行任務,能夠經過date.getTime()-System.currentTimeMillis()計算得出。

3、自定義線程池

自定義鏈接池稍微麻煩些,不過經過建立的ThreadPoolExecutor線程池對象,能夠獲取到當前線程池的尺寸、正在執行任務的線程數、工做隊列等進行線程池監控。

1. 線程池的建立

咱們能夠經過ThreadPoolExecutor來建立一個線程池, 建立自定義線程池的構造方法不少,如: 
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)

參數說明: java

corePoolSize:池中所保存的線程數,包括空閒線程。當提交一個任務到線程池時,線程池會建立一個線程來執行任務,即便其餘空閒的基本線程可以執行新任務也會建立線程,等到須要執行的任務數大於線程池基本大小時就再也不建立。若是調用了線程池的prestartAllCoreThreads方法,線程池會提早建立並啓動全部基本線程。 android

maximumPoolSize:線程池容許建立的最大線程數。若是隊列滿了,而且已建立的線程數小於最大線程數,則線程池會再建立新的線程執行任務。值得注意的是若是使用了無界的任務隊列這個參數就沒什麼效果。 數據庫

keepAliveTime(線程活動保持時間):線程池的工做線程空閒後,保持存活的時間。因此若是任務不少,而且每一個任務執行的時間比較短,能夠調大這個時間,提升線程的利用率。 數組

unitkeepAliveTime 參數的時間單位。可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。  緩存

workQueue:執行前用於保持任務的隊列。此隊列僅保持由 execute 方法提交的 Runnable 任務。能夠選擇如下幾個阻塞隊列:
ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。 
LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO (先進先出) 排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。 
SynchronousQueue:一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。 
PriorityBlockingQueue:一個具備優先級的無限阻塞隊列。 安全

2. 向線程池提交任務

咱們可使用execute提交的任務,可是execute方法沒有返回值,因此沒法判斷任務是否被線程池執行成功。經過如下代碼可知execute方法輸入的任務是一個Runnable類的實例。
threadsPool.execute(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
});
咱們也可使用submit 方法來提交任務,它會返回一個future,那麼咱們能夠經過這個future來判斷任務是否執行成功,經過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間後當即返回,這時有可能任務沒有執行完。
Future<Object> future = executor.submit(harReturnValuetask);
try {
     Object s = future.get();
} catch (InterruptedException e) {
    // 處理中斷異常
} catch (ExecutionException e) {
    // 處理沒法執行任務異常
} finally {
    // 關閉線程池
    executor.shutdown();
}

3. 線程池的關閉

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

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

4、合理配置線程池

要想合理的配置線程池,就必須首先分析任務特性,能夠從如下幾個角度來進行分析:
任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
任務的優先級:高,中和低。
任務的執行時間:長,中和短。
任務的依賴性:是否依賴其餘系統資源,如數據庫鏈接。


任務性質不一樣的任務能夠用不一樣規模的線程池分開處理。CPU密集型任務配置儘量小的線程,如配置Ncpu+1個線程的線程池。IO密集型任務則因爲線程並非一直在執行任務,則配置儘量多的線程,如2*Ncpu。混合型的任務,若是能夠拆分,則將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,若是這兩個任務執行時間相差太大,則不必進行分解。咱們能夠經過Runtime.getRuntime().availableProcessors()方法得到當前設備的CPU個數。 服務器

優先級不一樣的任務可使用優先級隊列PriorityBlockingQueue來處理。它可讓優先級高的任務先獲得執行,須要注意的是若是一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。

執行時間不一樣的任務能夠交給不一樣規模的線程池來處理,或者也可使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫鏈接池的任務,由於線程提交SQL後須要等待數據庫返回結果,若是等待的時間越長CPU空閒時間就越長,那麼線程數應該設置越大,這樣才能更好的利用CPU。

建議使用有界隊列,有界隊列能增長系統的穩定性和預警能力,能夠根據須要設大一點,好比幾千。有一次咱們組使用的後臺任務線程池的隊列和線程池全滿了,不斷的拋出拋棄任務的異常,經過排查發現是數據庫出現了問題,致使執行SQL變得很是緩慢,由於後臺任務線程池裏的任務全是須要向數據庫查詢和插入數據的,因此致使線程池裏的工做線程所有阻塞住,任務積壓在線程池裏。若是當時咱們設置成無界隊列,線程池的隊列就會愈來愈多,有可能會撐滿內存,致使整個系統不可用,而不僅是後臺任務出現問題。固然咱們的系統全部的任務是用的單獨的服務器部署的,而咱們使用不一樣規模的線程池跑不一樣類型的任務,可是出現這樣問題時也會影響到其餘任務。

5、線程池的監控

經過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候可使用:
taskCount:線程池須要執行的任務數量。
completedTaskCount:線程池在運行過程當中已完成的任務數量。小於或等於taskCount。
largestPoolSize:線程池曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
getPoolSize:線程池的線程數量。若是線程池不銷燬的話,池裏的線程不會自動銷燬,因此這個大小隻增不+ getActiveCount:獲取活動的線程數。


經過擴展線程池進行監控。經過繼承線程池並重寫線程池的beforeExecute,afterExecute和terminated方法,咱們能夠在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。如: 多線程

protected void beforeExecute(Thread t, Runnable r) { }


6、參考資料

http://www.trinea.cn/android/java-android-thread-pool/ http://www.infoq.com/cn/articles/java-threadPool
相關文章
相關標籤/搜索