![教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)](http://p2.ttnews.xyz/loading.gif)
多線程與線程池結構圖
前言
Java中線程池是運用最多的併發框架,幾乎所有併發的程序都可以使用線程池來完成。阿里的Java開發手冊中明確指出:
線程資源必須通過線程池提供,不允許在應用中自行顯示創建線程。
在實際的生產環境中,線程的數量必須得到控制,盲目的大量創建線程對系統性能是有傷害的,合理使用線程好處:
- 減少在創建和銷燬現場上所消耗的時間和系統資源
- 提高響應速度,無需創建可以直接運行
- 提高線程的可管理性。使用線程池可以進行統一分配,調優和監控,但是要做到合理利用線程池,必須對其原理了如指掌。
線程池工作原理
![教學筆記:多線程之線程池(關鍵Executors&ExecutorService)(六)](http://p2.ttnews.xyz/loading.gif)
創建線程池
JDK內部已經提供了Executors類,它扮演者線程池工廠的角色,通過它可以取得擁有特定功能的線程池,但是我們最好手動創建線程池。原因如下:
- Executors內部也是直接構造線程池對象,沒有額外的操作
- 手動創建線程池,我們更明白線程池的參數,方便調優。
- 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, BlockingQueueworkQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
參數如下:
- corePoolSize: 線程池的基本大小。當提交一個任務的時候,線程池就會創建一個新的線程執行任務,即使核心線程池中有空閒線程,也會新建,知道線程池中的數量等於corePoolSize就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啟動所有的線程。
- maximumPoolSize:線程池允許創建的最大線程數。當使用無界隊列的時候,這個參數就沒什麼效果了。
- keepAliveTime:線程池的工作線程空閒以後,保持存活的時間,如果任務多,並且任務執行時間段,可以調大時間,提高線程的利用率。
- unit 保活時間的單位
- workQueue: 任務隊列,用於保持或等待執行的任務阻塞隊列。有如下隊列可供選擇:
- ArrayBlockingQueue: 基於數組結構的有界隊列,此隊列按FIFO原則對元素進行排序
- LinkedBlockingQueue: 基於鏈表的阻塞隊列,FIFO原則,吞吐量通常高於ArrayBlockingQueue.
- SynchronousQueue: 不存儲元素的阻塞隊列。每個插入必須要等到另一個線程調用移除操作。
- PriorityBlockingQueue: 具有優先級的無阻塞隊列
- threadFactory:用於設置創建線程的工廠。
- handler:拒絕策略,當隊列線程池都滿了,必須採用一種策略來處理還要提交的任務。
在實際應用中,我們可以將信息記錄到日誌,來分析系統的負載和任務丟失情況JDK中提供了4中策略:
- AbortPolicy: 直接拋出異常
- CallerRunsPolicy: 只用調用者所在的線程來運行任務
- DiscardOldestPolicy: 丟棄隊列中最老的一個人任務,並執行當前任務。
- DiscardPolicy: 直接丟棄新進來的任務
知道如上參數,再去分析Executors框架,聰明的你一定知道是怎麼回事了。
執行任務
可以使用兩個方法:
- 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使用的是搶佔式線程調度。
最後
線程池與普通線程的區別不會太多,只是更好的利用了系統資源,任何使用到線程的地方都可以使用線程池來替代。關於線程池,我們就說到這裡。
閱讀更多 3T教育編程猿 的文章