概述
線程可認為是操作系統可調度的最小的程序執行序列,一般作為進程的組成部分,同一進程中多個線程可共享該進程的資源(如內存等)。JVM線程跟內核輕量級進程有一對一的映射關系,所以JVM中的線程是很寶貴的。
一般在工程上多線程的實現是基於線程池的。因為相比自己創建線程,多線程具有以下優點
- 線程是稀缺資源,使用線程池可以減少創建和銷毀線程的次數,每個工作線程都可以重復使用。
- 可以根據系統的承受能力,調整線程池中工作線程的數量,防止因為消耗過多內存導致服務器崩潰。
Executors存在什么問題
看阿里巴巴開發手冊並發編程這塊有一條:線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式。
Executors為什么存在缺陷
1. 線程池工作原理
當一個任務通過execute(Runnable)
方法欲添加到線程池時:
- 如果此時線程池中的數量小於
corePoolSize
,即使線程池中的線程都處於空閑狀態,也要創建新的線程來處理被添加的任務。 - 如果此時線程池中的數量等於
corePoolSize
,但是緩沖隊列workQueue
未滿,那么任務被放入緩沖隊列。 - 如果此時線程池中的數量大於
corePoolSize
,緩沖隊列workQueue
滿,並且線程池中的數量小於maximumPoolSize
,建新的線程來處理被添加的任務。 - 那么通過
handler
所指定的策略來處理此任務。也就是:處理任務的優先級為:核心線程corePoolSize
、任務隊列workQueue
、最大線程maximumPoolSize
,如果三者都滿了,使用handler
處理被拒絕的任務。 - 當線程池中的線程數量大於
corePoolSize
時,如果某線程空閑時間超過keepAliveTime
,線程將被終止。這樣,線程池可以動態的調整池中的線程數。
2.newFixedThreadPool分析
Java中的BlockingQueue
主要有兩種實現,分別是ArrayBlockingQueue
和 LinkedBlockingQueue
。
ArrayBlockingQueue
是一個用數組實現的有界阻塞隊列,必須設置容量。
LinkedBlockingQueue
是一個用鏈表實現的有界阻塞隊列,容量可以選擇進行設置,不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE
。
這里的問題就出在:不設置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE。也就是說,如果我們不設置LinkedBlockingQueue
的容量的話,其默認容量將會是Integer.MAX_VALUE
。
而newFixedThreadPool
中創建LinkedBlockingQueue
時,並未指定容量。此時,LinkedBlockingQueue
就是一個無邊界隊列,對於一個無邊界隊列來說,是可以不斷的向隊列中加入任務的,這種情況下就有可能因為任務過多而導致內存溢出問題。
3. newCachedThreadPool分析
結合上述流程圖,核心線程數=0,最大線程無限大,由於SynchronousQueue是一個緩存值為1的阻塞隊列。當有大量任務請求時,線程池會創建大量線程,造成OOM。
線程池參數詳解
1. 構造方法
/** * @param corePoolSize 核心線程數 * @param maximumPoolSize 最大線程數 * @param keepAliveTime 線程所允許的空閑時間 * @param unit 線程所允許的空閑時間的單位 * @param workQueue 線程池所使用的緩沖隊列 * @param handler 線程池對拒絕任務的處理策略 */ ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
2. 線程池拒絕策略
RejectedExecutionHandler(飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那么必須采取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。。以下是JDK1.5提供的四種策略。
- AbortPolicy:直接拋出異常
- CallerRunsPolicy:只用調用者所在線程來運行任務。
- DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務。
- DiscardPolicy:不處理,丟棄掉。
- 當然也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。如記錄日志或持久化不能處理的任務。
線程池正確打開方式
1. 創建線程池
避免使用Executors創建線程池,主要是避免使用其中的默認實現,那么我們可以自己直接調用ThreadPoolExecutor
的構造函數來自己創建線程池。在創建的同時,給BlockQueue
指定容量就可以了。
ThreadPoolExecutor executorService = new ThreadPoolExecutor(8, 16, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>(10));
2. 向線程池提交任務
我們可以使用execute提交的任務,但是execute方法沒有返回值,所以無法判斷任務知否被線程池執行成功。通過以下代碼可知execute方法輸入的任務是一個Runnable類的實例。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60)); threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("線程池無返回結果"); } });
我們也可以使用submit 方法來提交任務,它會返回一個future,那么我們可以通過這個future來判斷任務是否執行成功,通過future的get方法來獲取返回值,get方法會阻塞住直到任務完成,而使用get(long timeout, TimeUnit unit)方法則會阻塞一段時間后立即返回,這時有可能任務沒有執行完。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(60)); Future<String> future = threadPoolExecutor.submit(new Callable<String>() { @Override public String call() throws Exception { return "ok"; } }); System.out.println("線程池返回結果:" + future.get());
3. 關閉線程池
shutdown關閉線程池
方法定義:public void shutdown()
(1)線程池的狀態變成SHUTDOWN狀態,此時不能再往線程池中添加新的任務,否則會拋出RejectedExecutionException異常。
(2)線程池不會立刻退出,直到添加到線程池中的任務都已經處理完成,才會退出。
注意這個函數不會等待提交的任務執行完成,要想等待全部任務完成,可以調用:
public boolean awaitTermination(longtimeout, TimeUnit unit)
shutdownNow關閉線程池並中斷任務
方法定義:public List shutdownNow()
(1)線程池的狀態立刻變成STOP狀態,此時不能再往線程池中添加新的任務。
(2)終止等待執行的線程,並返回它們的列表;
(3)試圖停止所有正在執行的線程,試圖終止的方法是調用Thread.interrupt(),但是大家知道,如果線程中沒有sleep 、wait、Condition、定時鎖等應用, interrupt()方法是無法中斷當前的線程的。所以,ShutdownNow()並不代表線程池就一定立即就能退出,它可能必須要等待所有正在執行的任務都執行完成了才能退出。
4. 如何配置線程池大小
CPU密集型任務
該任務需要大量的運算,並且沒有阻塞,CPU一直全速運行,CPU密集任務只有在真正的多核CPU上才可能通過多線程加速 CPU密集型任務配置盡可能少的線程數量:
CPU核數+1個線程的線程池。
例如: CPU 16核,內存32G。線程數=16
IO密集型任務
IO密集型任務線程並不是一直在執行任務,則應配置盡可能多的線程,如CPU核數*2
某大廠設置策略:IO密集型時,大部分線程都阻塞,故需要多配置線程數:
CPU核數/(1-阻塞系數)
例如: CPU 16核, 阻塞系數 0.9 ------------->16/(1-0.9) = 160 個線程數。
此時非阻塞線程=16