近年來由於互聯網的興起,所以現在Java 面試題中也經常會問到線程池技術,所以今天我們就說一說面試中經常問道的知識點。
一 、 基礎知識
爲什麼要使用線程池呢?
在實際使用中,線程是很佔用系統資源的,如果對線程管理不善很容易導致系統問題。因此,在大多數併發框架中都會使用線程池技術來管理線程,那麼使用線程池管理線程主要有下面三點好處:
降低資源消耗。 通過複用已經存在的線程和降低線程關閉的次數來儘可能降低系統的消耗;
提升系統響應速度。通過複用線程,省去創建線程的過程,因此整體上提升了系統的響應速度。
提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統的資源,還會降低系統的穩定性,因此,需要使用線程池管理線程。
Java 線程的整體架構體系
Java中的線程池是通過Executor框架實現的,該框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor這幾個類 。
三個JDK 線程實現類
Executors.newFixedThreadPool(int):執行長期任務性能好,創建一個線程池,一個池中有N個固定線程,有固定線程數的線程池。
// 代碼實現 模擬十個人去銀行辦理業務,然後有5個窗口進行業務
ExecutorService executorService = Executors.newFixedThreadPool(5); //固定線程數
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 辦理業務");
});
}
executorService.shutdown();
Executors.newSingleThreadExecutor():一個一個任務的執行,一池一線程
// 代碼實現
ExecutorService executorService = Executors.newSingleThreadExecutor(); //一池子一個工作線程類似銀行只有一個窗口
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 辦理業務");
});
}
executorService.shutdown();
Executors.newCachedThreadPool(): 執行很
多短期任務,線程池根據需要創建新線程,但是先前構建的線程可用時將他重用起來,可擴容。
// 代碼實現
ExecutorService executorService = Executors.newCachedThreadPool(); //可擴容線程池
for (int i = 0; i < 10; i++) {
// Thread.sleep(20);
executorService.execute(()->{
System.out.println(Thread.currentThread().getName()+" 辦理業務");
});
}
executorService.shutdown();
以上三個實現方法是Java 給我們帶的,但是如果在面試的時候面試官問你線程池的技術你要是管說這三個實現,基本上你就掛了。其實我們看他們三個的底層實現都是一個類那就是ThreadPoolExecutor
ThreadPoolExecutor
那麼我們看看那個ThreadPoolExecutor 類裏面的內容,下面就是線程池的源碼,那我們現在就看看這些源碼。
我們先看一下線程池的主要參數,7個參數
corePoolSize :線程池中常駐核心線程數
maximumPoolSize:線程池中能夠容納同時執行最大的線程數,此值必須大於等於1
keepAliveTime: 多餘的空閒線程的存貨時間,達到keepAliveTime時,多餘線程會被銷燬直到剩下corePoolSize個線程數爲止。
unit:keepAliveTime 的單位
workQueue:任務隊列,被提交但未被執行的任務。
threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程,一般默認即可。
handler:拒絕策略,表示當隊列滿了,並且工作線程大於等於線程池中最大的線程數(maximumPoolSize)時如何拒絕執行的runable的策略。
以上7個參數我們都是必須要掌握的,而且要知道爲什麼這麼設置,所以接下來我們我們要看一下線程池的底層原理。
線程池的底層工作原理(以下很重要,包括上面的圖也是很重要的)
在創建線程池後,開始等待請求。
在調用execute()方法添加一個請求任務時,線程池會做出如下判斷:
2.1 如果正在運行的線程數量小於corePoolSize,那麼馬上會創建線程運行任務;
2.2 如果正在運行的線程數量大於或等於corePoolSize,那麼將這個任務放入隊列;
2.3 如果這個時候隊列滿了且正在運行的線程數量還小於maximumPoolSize,那麼還是要創建非線程立刻運行這個任務;
2.4 如果隊列滿了且正在運行的線程數量大於或者等於maximumPoolSize,那麼線程池會啓動飽和拒絕策略來執行。
當一個線程完成任務時,它會從隊列中取下一個任務來執行。
當一個線程無事可做超過一定的時間(keepAliveTime)時,線程會判斷:如果當前運行的線程數量大於corePoolSize,那麼這個線程就會被停掉。所以線程池的所有任務完成後,它最終會收縮到corePoolSize的大小。
以上時線程池執行的整體過程,下面要看一下線程的處理過程。
線程池用哪個?生產中如設置合理參數呢 ?
線程池的拒絕策略 。
什麼是線程池的拒絕策略呢:等待隊列已經排滿了,再也塞不下新任務了同時,線程池中的max線程也達到了,無法繼續爲新任務服務。這個是時候我們就需要拒絕策略機制合理的處理這個問題。
JDK內置的4個拒絕策略(以下內置拒絕策略均實現了
RejectedExecutionHandle接口)
AbortPolicy(默認):直接拋出RejectedExecutionException異常阻止系統正常運行
CallerRunsPolicy:“調用者運行”一種調節機制,該策略既不會拋棄任務,也不
會拋出異常,而是將某些任務回退到調用者,從而降低新任務的流量。
DiscardOldestPolicy:拋棄隊列中等待最久的任務,然後把當前任務加人隊列中嘗試再次提交當前任務。
DiscardPolicy:該策略默默地丟棄無法處理的任務,不予任何處理也不拋出異常。如果允許任務丟失,這是最好的一種策略。
工作中我們選擇什麼線程池呢? 這個是面試常問的考點,如果你要是回答JDK默認的三種線程池技術你就完蛋了,代表你只是瞭解一些,根本沒有用過。
答案是一個都不用,工作中只能用自定義的。
JDK 都提供了爲什麼不用呢?
//Executors.newFixedThreadPool() 源碼
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// Executors.newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 看看JDK給我們自帶的實現,之前我們說過7大參數,那麼現在看看5個參數 其中這些參數設計的及其不合理(newCachedThreadPool 中的maximumPoolSize設置的是Integer.MAX_VALUE,有些太大了,這樣會導致系統失敗的),根本不符合企業的要求,所以我們要自定義。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
3L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
for (int i = 0; i < 9; i++) {
//Thread.sleep(20);
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+" 辦理業務");
});
}
threadPoolExecutor.shutdown();
但是這些參數我們要怎麼設置呢? 其中最重要的就是配置最大線程數
如果線程池要處理的任務是cpu密集型,那麼最大的任務就是cpu核數+ 1(但是我們不能寫死了 要使用代碼自動獲取Runtime.getRuntime().availableProcessors())。
如果線程池要處理的任務是IO密集型,那麼最大的任務就是cpu核數/阻塞係數。