Java併發—線程池

線程池

Java中線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行任務的程序都可以使用線程池.在開發過程中, 合理使用線程池能夠帶來三個好處 :

  • 1.降低資源消耗. 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗
  • 2.提高響應速度.當任務到達時, 任務可以不需要等待線程創建就能立即執行
  • 3.提高線程的可管理性. 線程是稀缺資源, 如果無限制地創建, 不僅會消耗系統資源, 還會降低系統的穩定性, 使用線程池可以進行統一分配, 調優和監控.

1.線程池的實現原理

當向線程池提交一個任務之後, 線程池處理任務的流程如下 :

  • 1.線程池判斷核心線程池裏的線程是否都在執行任務. 如果不是, 則創建一個新的工作線程來執行任務. 如果核心線程池裏的線程都在執行任務,則進入下一個流程
  • 2.線程池判斷工作隊列是否已經滿, 如果工作隊列沒有滿, 則將新提交的任務存儲在這個工作隊列裏. 如果工作隊列滿了, 則進入下一個流程
  • 3.線程池判斷線程池的線程是否都處於工作狀態. 如果沒有, 則創建一個新的工作線程來執行任務. 如果已經滿了, 則交給飽和策略來處理這個任務

在這裏插入圖片描述

在這裏插入圖片描述

ThreadPoolExecuter執行execute方法分爲下面4種情況:

  • 1.如果當前運行的線程少於corePoolSize, 則創建新線程來執行任務(執行這一步驟需要獲取全局鎖)
  • 2.如果運行的線程等於或多於corePoolSize, 則將任務加入BlockingQueue
  • 3.如果無法將任務加入BlockingQueue(隊列已滿), 則創建新的線程來處理任務(執行這一步驟需要獲取全局鎖)
  • 4.如果創建新線程將使當前運行的線程超出maximumPoolSize, 任務將被拒絕, 並調用RejectedExecutionHandler.rejectedExecution()方法

ThreadPoolExecutor採取上述步驟的總體設計思路, 是爲了在執行execute()方法時, 儘可能地避免獲取全局鎖. 在ThreadPoolExecutor完成預熱之後(當前運行的線程數大於等於corePoolSize), 幾乎所有的execute()方法調用都是執行步驟2, 而步驟2不需要獲取全局鎖

工作線程 : 線程池創建線程時, 會將線程封裝成工作線程Worker, Worker在執行完任務後, 還會循環獲取工作隊列裏的任務來執行.

2.線程池的使用

1>線程池的創建

我們可以通過ThreadPoolExecutor來創建一個線程池

new ThreadPoolExecutor(corePoolSize, maximumPool, keepAliveTime, milliseconds, runnableTaskQueue, handler)

創建一個線程池時需要輸入幾個參數

  • 1.corePoolSize(核心線程池大小) : 當提交一個任務到線程池時, 線程池會創建一個線程來執行任務, 即使其他空閒的基本線程能夠執行新任務也會創建線程, 等到需要執行的任務數大於核心線程池大小時就不再創建. 如果調用了線程池的prestartAllCoreThreads()方法, 線程池會提前創建並啓動所有基本線程

  • 2.runnableTaskQueue(任務隊列) : 用於保存等待執行的任務的阻塞隊列, 可以選擇以下幾個阻塞隊列.

    • ArrayBlokingQueue : 是一個基於數組結構有界阻塞隊列, 此隊列按FIFO(先進先出)原則對元素進行排序
    • LinkedBlockingQueue : 一個基於鏈表結構的阻塞隊列, 此隊列按FIFO排序元素, 吞吐量通常要高於ArrayBlockingQueue. 靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
    • SynchronousQueue : 一個不存儲元素的阻塞隊列. 每個插入操作必須等到另一個線程調用移除操作, 否則插入操作會一直處於阻塞狀態, 吞吐量要高於LinkedBlockingQueue, 靜態工廠方法Executors.newCachedThreadPool使用了這個隊列
    • PriorityBlockingQueue : 一個具有優先級的無線阻塞隊列
  • 3.maximumPoolSize(線程池的最大數量) : 線程池允許創建的最大線程數. 如果隊列滿了, 並且已創建的線程數小於最大線程數, 則線程池會再創建新的線程執行任務.

  • 4.ThreadFactory : 用於設置創建線程的工廠, 可以通過線程工廠給每個創建出來的線程設置更有意義的名字

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

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

    也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略.如記錄日誌或持久化處處不能處理的任務

  • keepAliveTime(線程活動保持時間) : 線程池的工作線程空閒後, 保持存活的時間.

  • TimeUnit(線程活動保持時間的單位)

2>向線程池提交任務

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

execute()方法用於提交不需要返回值的任務, 所以無法判斷任務是否被線程池執行成功. execute()方法輸入的是一個Runnable類的實例

submit()方法用於提交需要返回值的任務. 線程池會返回一個future類型的對象, 通過這個future對象可以判斷任務是否成功執行, 並且可以通過future的get()方法獲取返回值, get()方法會阻塞當前線程直到任務完成

3>關閉線程池

可以通過調用線程池的shutdown或shutdownNow方法來關閉線程池, 他們的原理是遍歷線程池中的工作線程, 然後逐個調用線程的interrupt方法來中斷線程, 所以無法響應中斷的任務可能永遠無法終止,

只要調用了兩個關閉方法中的任意一個, isShutdown方法就會返回true. 當所有的任務都已關閉後, 才表示線程池關閉成功, 這時調用isTerminaed方法返回true. 至於應該調用哪一種方法來關閉線程池, 應該由提交到線程池的任務特性決定, 通常調用shutdown方法來關閉線程池, 如果任務不一定要執行完, 則可以調用shutdownNow方法

4>合理配置線程池

性質不同任務可以通過不同規模的線程池分開處理.

CPU密集型 : 應配置儘可能小的線程, 如配置Ncpu+1個線程的線程池.

IO密集型 : 因爲並不是一直在執行任務, 則應配置儘可能多的線程, 如2*Ncpu

優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理, 它可以讓優先級高的任務執行

執行時間不同的任務可以交給不同規模的線程池來處理, 或者可以使用優先級隊列, 讓執行時間短的任務先執行

依賴數據庫連接池的任務, 因爲線程提交SQL後需要等待數據庫返回結果, 等待的時間越長, 則CPU空閒時間就越長, 那麼線程數應該設置更大, 這樣才能更好地利用CPU

5>線程池中的監控

如果在系統中大量使用線程池, 則有必要對線程池進行監控, 方便在出問題時, 可以根據線程池的使用情況快速定位問題. 可以通過線程池提供的參數進行監控, 在監控線程池的時候可以使用以下屬性

  • taskCount : 線程池需要執行的任務數量
  • completedTaskCount : 線程池在運行過程中已完成的任務數量, 小於或等於taskCount
  • largestPoolSize : 線程池裏曾經創建的最大線程數量]
  • getPoolSize : 線程池的線程數量
  • getActiveCount : 獲取活動的線程數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章