教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)

教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)

多線程與線程池結構圖

前言

Java中線程池是運用最多的併發框架,幾乎所有併發的程序都可以使用線程池來完成。阿里的Java開發手冊中明確指出:

線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程。

在實際的生產環境中,線程的數量必須得到控制,盲目的大量創建線程對系統性能是有傷害的,合理使用線程好處:

  • 減少在創建和銷燬現場上所消耗的時間和系統資源
  • 提高響應速度,無需創建可以直接運行
  • 提高線程的可管理性。使用線程池可以進行統一分配,調優和監控,但是要做到合理利用線程池,必須對其原理了如指掌。

線程池工作原理

教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)

創建線程池

JDK內部已經提供了Executors類,它扮演者線程池工廠的角色,通過它可以取得擁有特定功能的線程池,但是我們最好手動創建線程池。原因如下:

  1. Executors內部也是直接構造線程池對象,沒有額外的操作
  2. 手動創建線程池,我們更明白線程池的參數,方便調優。
  3. Executors創建的線程池有可能導致OOM異常。

雖然不建議直接使用Executors直接創建線程池,但是我們可以看一下它給我們提供了那些工廠方法:

 // 返回一個可根據實際情況調整線程數量的線程池 // 它是大小無界的線程池,適合執行很多短期一步的小程序,或是負載比較輕的服務器。 public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }  // 返回一個固定線程數量的線程池 // 適合負載比較重的服務器 public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }  // 返回一個固定線程數量的線程池對象,ScheduledThreadPoolExecutor對象可以定時執行某任務 // 適合於多個後臺線程執行週期任務。 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }  // 返回只有一個線程的線程池。 // 適合於單個線程順序的執行任務 public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue 
())); } // 返回只有一個線程的ScheduledThreadPoolExecutor對象。 // 單個線程執行週期任務 public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); }

本質上,我們可以通過ThreadPoolExecutor來創建線程池:

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {

參數如下:

  1. corePoolSize: 線程池的基本大小。當提交一個任務的時候,線程池就會創建一個新的線程執行任務,即使核心線程池中有空閒線程,也會新建,知道線程池中的數量等於corePoolSize就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啟動所有的線程。
  2. maximumPoolSize:線程池允許創建的最大線程數。當使用無界隊列的時候,這個參數就沒什麼效果了。
  3. keepAliveTime:線程池的工作線程空閒以後,保持存活的時間,如果任務多,並且任務執行時間段,可以調大時間,提高線程的利用率。
  4. unit 保活時間的單位
  5. workQueue: 任務隊列,用於保持或等待執行的任務阻塞隊列。有如下隊列可供選擇:
  • ArrayBlockingQueue: 基於數組結構的有界隊列,此隊列按FIFO原則對元素進行排序
  • LinkedBlockingQueue: 基於鏈表的阻塞隊列,FIFO原則,吞吐量通常高於ArrayBlockingQueue.
  • SynchronousQueue: 不存儲元素的阻塞隊列。每個插入必須要等到另一個線程調用移除操作。
  • PriorityBlockingQueue: 具有優先級的無阻塞隊列
  1. threadFactory:用於設置創建線程的工廠。
  2. handler:拒絕策略,當隊列線程池都滿了,必須採用一種策略來處理還要提交的任務。

在實際應用中,我們可以將信息記錄到日誌,來分析系統的負載和任務丟失情況JDK中提供了4中策略:

  • AbortPolicy: 直接拋出異常
  • CallerRunsPolicy: 只用調用者所在的線程來運行任務
  • DiscardOldestPolicy: 丟棄隊列中最老的一個人任務,並執行當前任務。
  • DiscardPolicy: 直接丟棄新進來的任務

知道如上參數,再去分析Executors框架,聰明的你一定知道是怎麼回事了。

教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)

執行任務

可以使用兩個方法:

  • execute() 提交不需要返回值的任務,無法判斷是否執行成功,具體步驟上面我們有分析
  • submit() 提交有返回值的任務,該方法返回一個future的對象,通過future對象可以判斷任務是否執行成功。future的get方法會阻塞當前線程直到任務完成。

關閉線程池

兩個方法:

  • shutdown() 通知線程該結束了,嘗試用終端來停止線程,如果線程對中斷不響應的話,那麼這個方法無法關閉線程池。
  • shutdownNow() 看名字就知道是立刻關閉線程池,類似於線程的stop方法,不等待任務執行完成就關閉線程。

擴展線程池

有時候需要對線程池做一些擴展,比如知道線程池的開始結束時間,線程池的運行統計等信息。這個時候好在ThreadPoolExecutor給我們提供了三個方法進行擴展:

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

可以監控的屬性:

  • taskCount: 線程池需要執行的任務數量
  • completedTaskCount: 已經完成的任務數量
  • largestPoolSize: 線程池中曾經創建的最大的線程數量
  • getPoolSize: 線程池的線程數量
  • getActiveCount: 活動的線程數

合理配置線程池

線程池中線程的數量過大和過小都無法使系統的性能發揮到最優,確定線程池的大小可以考慮下面的角度:

  • 任務性質:CPU密集,IO密集,和混合密集
  • 任務執行時間:長,中,低
  • 任務優先級:高,中,低
  • 任務的依賴性:是否依賴其它資源,如數據庫連接

建議使用有界隊列,防止撐爆內存

在Java中,獲取CPU數量:

Runtime.getRuntime().availableProcessors(); 

線程池計算公式:

N = CPU數量U = 目標CPU使用率, 0 <= U <= 1W/C = 等待(wait)時間與計算(compute)時間的比率線程池數量 = N * U * (1 + W/C)

線程調度

在多線程競爭的情況下,肯定要涉及到線程調度的問題。線程調度是指系統為線程分配處理器的過程,主要調度方式有兩種。

  • 協同步式線程調度(Cooperative Threads-Schedulin) :線程的執行時間由線程本身控制,執行完任務之後通知系統切換到另外一個線程上。 實現簡單,但是如果一個線程堅持不讓出CPU,那麼會導致整個系統崩潰。
  • 搶佔式線程調度(Preemptive Threads-Schedulng):由系統分配時間,線程切換不由線程本身決定,這種情況下線程的執行時間是系統可控的,也不會有某線程出現問題導致進程阻塞的問題。

Java使用的是搶佔式線程調度。

最後

線程池與普通線程的區別不會太多,只是更好的利用了系統資源,任何使用到線程的地方都可以使用線程池來替代。關於線程池,我們就說到這裡。


教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)


分享到:


相關文章: