Java併發(四)線程池使用

上一篇博文介紹了線程池的實現原理,如今介紹如何使用線程池。數據庫

目錄

  1、建立線程池數組

  2、向線程池提交任務服務器

  3、關閉線程池框架

  4、合理配置線程池ide

  5、線程池的監控ui

  線程池建立規範spa

1、建立線程池

咱們能夠經過ThreadPoolExecutor來建立一個線程池。線程

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
timeUnit, runnableTaskQueue, threadFactory, handler);

建立一個線程池時須要輸入如下幾個關鍵參數:rest

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

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

3. runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。能夠選擇如下幾個阻塞隊列。

隊列 描述
ArrayBlockingQueue 一個基於數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序。
LinkedBlockingQueue 一個基於鏈表結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
SynchronousQueue 一個不存儲元素的阻塞隊列。每一個插入操做必須等到另外一個線程調用移除操做,不然插入操做一直處於阻塞狀態,吞吐量一般要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列。
PriorityBlockingQueue 一個具備優先級的無限阻塞隊列。

4. threadFactory:用於設置建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder能夠快速給線程池裏的線程設置有意義的名字,代碼以下。

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

若是不想使用ThreadFactoryBuilder,也能夠自定義線程工廠類:

/**
 * 生成線程池所用的線程,只是改寫了線程池默認的線程工廠,傳入線程池名稱,便於問題追蹤
 */
static class EventThreadFactory implements ThreadFactory {

    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    /**
     * 線程所屬的線程組
     */
    private final ThreadGroup group;
    /**
     * 用AtomicInteger來爲線程計數,每次加1
     */
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    /**
     * 線程名稱
     */
    private final String namePrefix;

    /**
     * 初始化線程工廠
     *
     * @param poolName 線程池名稱
     */
    EventThreadFactory(String poolName) {
        SecurityManager s = System.getSecurityManager();
        group = Objects.nonNull(s) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        // 命名線程
        namePrefix = poolName + "-pool-" + poolNumber.getAndIncrement() + "-thread-";
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
        if (t.isDaemon())
            t.setDaemon(false);
        // 設置相同的線程優先級,避免線程池裏的線程根據優先級爭搶資源,保證任務的正常執行
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

5. RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採起一種策略處理提交的新任務。這個策略默認狀況下是AbortPolicy,表示沒法處理新任務時拋出異常。在JDK 1.5中Java線程池框架提供瞭如下4種策略。

策略 描述
AbortPolicy 直接拋出異常。
CallerRunsPolicy 只用調用者所在線程來運行任務。
DiscardOldestPolicy 丟棄隊列裏最近的一個任務,並執行當前任務。
DiscardPolicy 不處理,丟棄掉。

固然,也能夠根據應用場景須要來實現RejectedExecutionHandler接口自定義策略。如記錄日誌或持久化存儲不能處理的任務。

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

7. timeUnit(線程活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)。

2、向線程池提交任務

可使用兩個方法向線程池提交任務,分別爲execute()和submit()方法。

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、線程池的監控

若是在系統中大量使用線程池,則有必要對線程池進行監控,方便在出現問題時,能夠根據線程池的使用情況快速定位問題。能夠經過線程池提供的參數進行監控,在監控線程池的時候可使用如下屬性:

屬性 描述
getTaskCount() 線程池須要執行的任務數量。
getCompletedTaskCount() 線程池在運行過程當中已完成的任務數量,小於或等於taskCount。
getLargestPoolSize() 線程池裏曾經建立過的最大線程數量。經過這個數據能夠知道線程池是否曾經滿過。如該數值等於線程池的最大大小,則表示線程池曾經滿過。
getPoolSize() 線程池的線程數量。若是線程池不銷燬的話,線程池裏的線程不會自動銷燬,因此這個大小隻增不減。
getActiveCount() 獲取活動的線程數。

經過擴展線程池進行監控。能夠經過繼承線程池來自定義線程池,重寫線程池的beforeExecute、afterExecute和terminated方法,也能夠在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。

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

線程池建立規範

良好的開端是成功的一半。如下內容摘自阿里巴巴的Java開發手冊,對咱們排查線程池問題有必定幫助:

1.【強制】線程資源必須經過線程池提供,不容許在應用中自行顯式建立線程。

說明:使用線程池的好處是減小在建立和銷燬線程上所花的時間以及系統資源的開銷,解決資源不足的問題。若是不使用線程池,有可能形成系統建立大量同類線程而致使消耗完內存或者 「過分切換」 的問題。

2. 線程池不容許使用 Executors 去建立,而是經過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors 各個方法的弊端:

  • newFixedThreadPool 和 newSingleThreadExecutor: 主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至 OOM。

  • newCachedThreadPool 和 newScheduledThreadPool: 主要問題是線程數最大數是 Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至 OOM。

3.【強制】建立線程或線程池時請指定有意義的線程名稱,方便出錯時回溯。

public class TimerTaskThread extends Thread {
    public TimerTaskThread(){
        super.setName("TimerTaskThread"); ...
    }
}
相關文章
相關標籤/搜索