高並發麵試:線程池的七大參數?手寫一個線程池?
線程池
1. Callable接口的使用
package com.yuxue.juc.threadPool;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 多線程中,第三種獲得多線程的方式
* */
public class CallableTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//FutureTask(Callable<V> callable)
FutureTask<Integer> futureTask = new FutureTask<>(new myThread());
new Thread(futureTask, "AAA").start();
//new Thread(futureTask, "BBB").start();//復用,直接取值,不要重啟兩個線程
int a = 100;
int b = 0;
//b = futureTask.get();//要求獲得Callable線程的計算結果,如果沒有計算完成就要去強求,會導致堵塞,直到計算完成
while (!futureTask.isDone()) {
////當futureTask完成後取值
b = futureTask.get();
}
System.out.println("===Result is:" + (a + b));
}
}
class myThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "\tget in the callable");
Thread.sleep(5000);
return 1024;
}
}
兩者區別:
- Callable:有返回值,拋異常
- Runnable:無返回值,不拋出異常
2. 為什麼要使用線程池
-
線程池做的工作主要是控制運行的線程的數量,處理過程中將任務放入隊列,然後在線程創建後啟動給這些任務,如果線程數量超過了最大數量,超出數量的線程排隊等候,等其他線程執行完畢,再從隊列中取出任務來執行
-
主要特點
線程復用、控制最大並發數、管理線程
- 減少創建和銷毀線程上所花的時間以及系統資源的開銷 => 減少內存開銷,創建線程佔用內存,創建線程需要時間,會延遲處理的請求;降低資源消耗,通過重複利用已創建的線程降低線程創建和銷毀造成的消耗
- 提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行
- 提高線程的客觀理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控(根據系統承受能力,達到運行的最佳效果) => 避免無限創建線程引起的
OutOfMemoryError
【簡稱OOM】
3. 線程池如何使用?
-
架構說明
Java中的線程池是通過Executor框架實現的,該框架中用到了 :
Executor,Executors,ExecutorService,ThreadPoolExecutor
-
編碼實現
實現有五種,
Executors.newScheduledThreadPool()
是帶時間調度的,java8新推出Executors.newWorkStealingPool(int)
,使用目前機器上可用的處理器作為他的並行級別重點有三種
-
Executors.newFixedThreadPool(int)
執行長期的任務,性能好很多
創建一個定長線程池,可控制線程最大並發數,超出的線程回在隊列中等待newFixedThreadPool創建的線程池corePoolSize和maximumPoolSize值是相等的,它使用的是 LinkedBlockingQueue
底層源碼:
-
Executors.newSingleThreadExecutor()
一個任務一個任務執行的場景
創建一個單線程話的線程池,他只會用唯一的工作線程來執行任務,保證所有任務按照指定順序執行
newSingleThreadExecutor將corePoolSize和maximumPoolSize都設置為1,使用LinkedBlockingQueue
源碼:
-
Executors.newCachedThreadPool()
執行很多短期異步的小程序或負載較輕的服務器
創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑縣城,若無可回收,則新建線程
newCachedThreadPool將corePoolSize設置為0,將maximumPoolSize設置為Integer.MAX_VALUE,使用 的SynchronousQueue,也就是說來了任務就創建線程運行,當縣城空閑超過60s,就銷毀線程
源碼:
我們可以看到底層的代碼都是由ThreadPoolExecutor這個類的構造方法創建的,只是傳入的參數不同,那麼研究一下這個類以及這些參數就很有必要,下節我們將介紹這些參數的使用
-
-
ThreadPoolExecutor
4. 線程池的幾個重要參數
核心執行代碼為:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
那麼我們再點進this方法可以看到其全參數的構造方法為:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
簡單介紹一下:
-
corePoolSize: 線程池中常駐核心線程數
- 在創建了線程池後,當有請求任務來之後,就會安排池中的線程去執行請求任務
- 當線程池的線程數達到corePoolSize後,就會把到達的任務放到緩存隊列當中
-
maximumPoolSize: 線程池能夠容納同時執行的最大線程數,必須大於等於1
-
keepAliveTime: 多餘的空閑線程的存活時間
- 當前線程池數量超過corePoolSize時,檔口空閑時間達到keepAliveTime值時,多餘空閑線程會被銷毀到只剩下corePoolSize個線程為止
-
unit: keepAliveTime的單位
-
workQueue: 任務隊列,被提交但尚未被執行的任務
任務隊列底層是BlockingQueue阻塞隊列!不清楚阻塞隊列的參考這篇文章:用阻塞隊列實現一個生產者消費者模型?
-
threadFactory:表示生成線程池中工作線程的線程工廠,用於創建線程一般用默認的即可
-
handler: 拒絕策略,表示當隊列滿了並且工作線程大於等於線程池的最大線程數(maximumPoolSize)時如 何來拒絕請求執行的runable的策略
5. 線程池的底層工作原理以及過程
如上圖所屬,其流程為:
-
在創建了線程池之後,等待提交過來的任務請求
-
當調用execute()方法添加一個請求任務時,線程池會做出如下判斷:
2.1 如果正在運行的線程數量小於corePoolSize,那麼馬上創建線程運行這個任務;
2.2 如果正在運行的線程數量大於或等於corePoolSize,那麼將這個任務放入隊列;
2.3 如果此時隊列滿了且運行的線程數小於maximumPoolSize,那麼還是要創建非核心線程立刻運行此任務;
2.4 如果隊列滿了且正在運行的線程數量大於或等於maxmumPoolSize,那麼啟動飽和拒絕策略來執行
-
當一個線程完成任務時,他會從隊列中取出下一個任務來執行
-
當一個線程無事可做超過一定的時間(keepAliveTime)時,線程池會判斷:
如果當前運行的線程數大於corePoolSize,那麼這個線程會被停掉;所以線程池的所有任務完成後他最大會收 縮到corePoolSize的大小
5. 實際工作中如何設置合理參數
5.1 線程池的拒絕策略
- 什麼是線程的拒絕策略
- 等待隊列也已經排滿了,再也塞不下新任務了,同時線程池中的max線程也達到了,無法繼續為新任務服務。這時我們就需要拒絕策略機制合理的處理這個問題
- JDK內置的拒絕策略
- AbortPolicy(默認):如果滿了會直接拋出RejectedExecutionException異常阻止系統正常運行
- CallerRunsPolicy:」調用者運行「一種調節機制,該策略既不會拋棄任務,也不會拋出異常,而是將某些任務回退到調用者, 從而降低新任務的流量
- DiscardOldestPolicy:拋棄隊列中等待最久的任務,然後把當前任務加入隊列中嘗試再次提交當前任務
- DiscardPolicy:直接丟棄任務,不予任何處理也不拋異常。如果允許任務丟失,這是最好的一種方案
- 均實現了RejectedExecutionHandler接口
5.2 你在工作中單一的/固定數的/可變的三種創建線程池的方法,用哪個多?
回答:一個都不用,我們生產上只能使用自定義的!!!!
為什麼?
線程池不允許使用Executors創建,試試通過ThreadPoolExecutor的方式,規避資源耗盡風險
阿里巴巴規範手冊當中提到:
- FixedThreadPool和SingleThreadPool允許請求隊列長度為Integer.MAX_VALUE,可能會堆積大量請求,導致OOM;
- CachedThreadPool和ScheduledThreadPool允許的創建線程數量為Integer.MAX_VALUE,可能會創建大量線程,導致OOM
5.3 你在工作中時如何使用線程池的,是否自定義過線程池?
上代碼:
package com.yuxue.juc.threadPool;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
//corePoolSize:常駐核心線程數
2,
//maximumPoolSize:最大的可容納線程數
5,
//存活時間設置為1s
1L,
TimeUnit.SECONDS,
//這裡用LinkedBlockingQueue,且容量為3,意味着等候區最大容量三個任務
new LinkedBlockingQueue<>(3),
//默認的defaultThreadFactory即可
Executors.defaultThreadFactory(),
//丟棄方法使用AbortPolicy()
new ThreadPoolExecutor.AbortPolicy());
//這裡用來做任務的處理執行
for (int i = 0; i < 5; i++) {
threadPoolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"\t 辦理業務;");
});
}
threadPoolExecutor.shutdown();
}
}
我們運行結果為:
pool-1-thread-2 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
//可以看到當我們任務書為5且處理速度非常快時,我們就用核心的corePoolSize就可以滿足任務需求
當任務數量變多或者任務變重時:如將我們的任務數量調整為20時,此時運行結果為:
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-4 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-2 辦理業務;
pool-1-thread-5 辦理業務;
pool-1-thread-4 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-3 辦理業務;
pool-1-thread-1 辦理業務;
pool-1-thread-2 辦理業務;
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.yuxue.juc.threadPool.MyThreadPoolDemo$$Lambda$1/558638686@6d03e736 rejected from java.util.concurrent.ThreadPoolExecutor@568db2f2[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 15]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.yuxue.juc.threadPool.MyThreadPoolDemo.main(MyThreadPoolDemo.java:27)
發生了異常,且任務只執行完了15個,我們可以看到其中active threads = 0, queued tasks = 0也就是說我的阻塞隊列已經滿了,且沒有空閑的線程了,此時再申請任務我就會拋出異常,這是線程池handler參數的拒絕策略,當我們更改策略為ThreadPoolExecutor.CallerRunsPolicy()
時,運行結果當中存在main 辦理業務;
語句,也就意味着線程池將某些任務回退到了調用者,另外的兩個拒絕策略在此就不演示
5.4 合理配置線程池你是如何考慮的?
-
CPU密集型:
- CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速運行
- CPU密集任務只有在真正多核CPU上才可能得到加速(通過多線程) 而在單核CPU上,無論你開幾個模擬的多線程該任務都不可能得到加速,因為CPU總的運算能力就那些
- CPU密集型任務配置儘可能少的線程數量
一般公式:CPU核數+1個線程的線程池
-
IO密集型
- 由於IO密集型任務線程並不是一直在執行任務,則應配置經可能多的線程,如CPU核數 * 2
- IO密集型,即該任務需要大量的IO,即大量的阻塞。在單線程上運行IO密集型的任務會導致浪費大量的 CPU運算能力浪費在等待。
- 所以在IO密集型任務中使用多線程可以大大的加速程序運行,即使在單核CPU上,這種加速主要就是利用 了被浪費掉的阻塞時間。
- IO密集型時,大部分線程都阻塞,故需要多配置線程數:
參考公式:CPU核數/(1-阻塞係數) 阻塞係數在0.8~0.9之間
例如八核CPU:,利用公式,約為:8/(1-0,9)=80個線程