一、什么是線程池?
線程池做的工作主要是控制運行的線程的數量,處理過程中將任務加入隊列,然后在線程創建后啟動這些任務,如果線程數超過了最大數量,超出的數量的線程排隊等候,等其他線程執行完畢,再從隊列中取出任務來執行。
線程池的主要特點為:線程復用、控制最大並發數、管理線程。
二、線程池的優勢
- 降低資源消耗。通過重復利用自己創建的線程,降低線程創建和銷毀造成的消耗。
- 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性。線程是稀缺資源,如果無限的創建,不僅會消耗資源,還會較低系統的穩定性,使用線程池可以進行統一分配,調優和監控。
三、架構實現
Java 中的線程池是通過 Executor 框架實現的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor 這幾個類。
四、線程池的幾種創建方式
4.0 ThreadPoolExecutor()
最原始的線程池創建,下面的前三種創建方式都是對 ThreadPoolExecutor 的封裝。
4.1 Executors.newFixedThreadPool(int)
主要特點如下:
1. 創建一個定長線程池,可控制線程的最大並發數,超出的線程會在隊列中等待.
2. newFixedThreadPool 創建的線程池 corePoolSize 和 MaxmumPoolSize 是相等的,它使用的的LinkedBlockingQueue。
4.2 Executors.newSingleThreadExecutor()
主要特點如下:
1. 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務都按照指定順序執行。
2. newSingleThreadExecutor 將 corePoolSize 和 MaxmumPoolSize 都設置為 1,它使用的是LinkedBlockingQueue。
4.3 Executors.newCachedThreadPool()
主要特點如下:
1. 創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程;若無可回收,則創建新線程。
2. newCachedThreadPool 將 corePoolSize 設置為 0,MaxmumPoolSize 設置為 Integer.MAX_VALUE,它使用的是 SynchronousQueue,也就是說來了任務就創建線程運行,如果線程空閑超過60秒,就銷毀線程。
4.4 Executors.newWorkStealingPool()
Java 8 加入的創建方法,其內部會構建ForkJoinPool,利用Work-Stealing算法,並行地處理任務,不保證處理順序。
4.5 生產中的使用
實際生產中不使用 Executors 創建線程池,需要使用 ThreadPoolExecutor 自定義創建。
阿里巴巴開發手冊中關於並發處理的部分內容:
自定義創建線程池:
1 public static void main(String[] args) { 2 ExecutorService threadPool = new ThreadPoolExecutor( 3 2, 4 5, 5 1L, 6 TimeUnit.SECONDS, 7 new LinkedBlockingQueue<>(3), 8 Executors.defaultThreadFactory(), 9 new ThreadPoolExecutor.AbortPolicy()); 10 11 try { 12 for (int i = 1; i <= 10; i++) { 13 threadPool.execute(() -> { 14 System.out.println(Thread.currentThread().getName() + "\t 辦理業務"); 15 }); 16 } 17 } finally { 18 threadPool.shutdown(); 19 } 20 }
五、線程池中的幾個重要參數
- corePoolSize:線程池中的常駐核心線程數。
- maximumPoolSize:線程池能夠容納的同時執行的最大線程數,此值大於等於 1。
- keepAliveTime:多余的空閑線程存活時間,當空閑時間達到 keepAliveTime 值時,多余的線程會被銷毀,直到只剩下 corePoolSize 個線程為止。
- unit:keepAliveTime 的單位。
- workQueue:任務隊列,被提交但尚未被執行的任務。
- threadFactory:表示生成線程池中工作線程的線程工廠,用戶創建新線程,一般用默認即可。
- handler:拒絕策略,表示當線程隊列滿了並且工作線程大於等於線程池的最大線程數(maxnumPoolSize)時如何來拒絕。
六、線程池的底層工作原理
1. 在創建了線程池后,等待提交過來的任務請求。
2. 當調用 execute() 方法添加一個請求任務時,線程池會做如下判斷:
2.1 如果正在運行的線程數量小於 corePoolSize,那么馬上創建線程運行這個任務;
2.2 如果正在運行的線程數量大於或等於 corePoolSize,那么將這個任務放入隊列;
2.3 如果這時候隊列滿了且正在運行的線程數量還小於 maximumPoolSize,那么還是要創建非核心線程立刻運行這個任務;
2.4 如果隊列滿了且正在運行的線程數量大於或等於 maximumPoolSize,那么線程池會啟動飽和拒絕策略來執行。
3. 當一個線程完成任務時,它會從隊列中取下一個任務來執行。
4. 當一個線程無事可做,超過一定的時間(keepAliveTime) 時,線程池會判斷:
如果當前運行的線程數大於 corePoolSize,那么這個線程就被停掉。
所以線程池的所有任務完成后,它最終會收縮到 corePoolSize 的大小。
七、線程池的拒絕策略
當線程池中工作線程數達到 maximumPoolSize,且工作隊列已滿,那么就會執行拒絕策略處理任務,jdk 內置有四種拒絕策略。
拒絕策略 | 拒絕行為 |
AbortPolicy |
直接拋出 RejectedException 異常阻止系統正常運行 |
CallerRunsPolicy |
"調用者運行"一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是由提交任務者執行這個任務 |
DiscardOldestPolicy |
拋棄隊列中等待最久的任務,然后把當前任務加入隊列中嘗試再次提交 |
DiscardPolicy |
直接丟棄任務,不予任何處理也不拋出異常。如果允許任務丟失,這是最好的拒絕策略 |
八、合理配置線程池參數
首先,通過 System.out.println(Runtime.getRuntime().availableProcessors()); 獲得當前計算機的 CPU 核數。
8.1 CPU 密集型
CPU 密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行。
CPU 密集任務只有在真正的多核 CPU 上才可能得到加速(通過多線程),而在單核CPU上,無論開幾個模擬的多線程,該任務都不可能得到加速,因為CPU總的運算能力就那些。
CPU 密集型任務配置盡可能少的線程數量:
一般公式:CPU核數+1個線程的線程池
8.2 IO密集型
- 由於 IO 密集型任務線程並不是一直在執行任務,則應配置盡可能多的線程,如 CPU 核數 * 2。
- IO密集型,即該任務需要大量的IO,即大量的阻塞。在單線程上運行 IO 密集型的任務會導致浪費大量的 CPU 運算能力在等待上。所以在 IO 密集型任務中使用多線程可以大大的加速程序運行,即使在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
IO 密集型時,大部分線程都阻塞,故需要多配置線程數:
參考公式:CPU核數 / (1-阻塞系數) 阻塞系數在0.8~0.9之間
比如 8 核CPU:8 / (1 - 0.9) = 80 個線程數