一、什麼是執行緒池
執行緒池:指在初始化一個多執行緒應用程式過程中建立一個執行緒集合,然後在需要執行新的任務時重用這些執行緒而不是新建一個執行緒,
一旦任務已經完成了,執行緒回到池子中並等待下一次分配任務。
二、使用執行緒池的好處
1)控制最大並大數。
2)降低資源消耗。通過重複利用已建立的執行緒來降低執行緒建立和銷燬造成的消耗。
3)提高響應速度。當任務到達時,任務不需要等到執行緒建立,而是可以直接使用執行緒池中的空閒執行緒。
4)提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配、延時執行、調優和監控等。
三、涉及到的類和介面
常用的執行緒池介面和類都在 java.util.concurrent包下,大致為:
Executor:執行緒池的頂級介面
ExecutorService:執行緒池介面,可通過submit()方法提交任務程式碼
ExecutorService介面的實現類最常用的為以下兩個:
ThreadPoolExecutor
ScheduledThreadPoolExecutor
和 Array -> Arrays、Collection -> Collections 一樣,執行緒池的建立也是有工具類可以使用的:
Executors工廠類:通過此類可以建立一個執行緒池
四、執行緒池種類
在 JDK 8 以後,一共有 5 種執行緒池,分別為:
固定執行緒數的執行緒池
只有一個執行緒的執行緒池
可根據任務數動態擴容執行緒數的執行緒池
可排程的執行緒池
具有搶佔式操作的執行緒池
這些執行緒池都能由 Executors 工具類來進行建立,分別對應以下方法:
1)newFixedThreadPool:建立指定的、固定個數的執行緒池
2)newCachedThreadPool:建立快取執行緒池(執行緒個數根據任務數逐漸增加,上線為 Integer.MAX_VALUE)
3)newSingleThreadExecutor:建立單個執行緒的執行緒池
4)newScheduledThreadPool:建立可排程的執行緒池 排程:定時、週期執行5)newWorkStealingPool:建立具有搶佔式操作的執行緒池
對於 newWorkStealingPool 的補充:
newWorkStealingPool,這個是 JDK1.8 版本加入的一種執行緒池,stealing 翻譯為搶斷、竊取的意思,它實現的一個執行緒池和上面4種都不一樣,用的是 ForkJoinPool 類。
newWorkStealingPool 適合使用在很耗時的操作,但是 newWorkStealingPool 不是 ThreadPoolExecutor 的擴充套件,它是新的執行緒池類 ForkJoinPool 的擴充套件,但是都是在統一的一個 Executors 類中實現,由於能夠合理的使用 CPU 進行任務操作(並行操作),所以適合使用在很耗時的任務中。
參考文章:
https://blog.csdn.net/qq_38428623/article/details/86689800
https://blog.csdn.net/tjbsl/article/details/98480843
五、如何使用執行緒池
(一)使用步驟
1)建立執行緒池物件
2)建立執行緒任務
3)使用執行緒池物件的 submit() 或者 execute() 方法提交要執行的任務
4)使用完畢,可以使用shutdown()方法關閉執行緒池
(二)案例程式碼
需求:使用執行緒池管理執行緒來簡單的模擬買票程式。
public class Demo(){
public static void main(String[] args) {
test();
}
public static void test(){
//1、建立執行緒池物件
ExecutorService pool = Executors.newFixedThreadPool(4);
//2、建立任務
Runnable runnable = new Runnable(){
private int tickets = 100;
@Override
public void run() {
while (true){
if(tickets <= 0){
break;
}
System.out.println(Thread.currentThread().getName()+"賣了第"+tickets+"張票");
tickets--;
}
}
};
//3、將任務提交到執行緒池(需要幾個執行緒來執行就提交幾次)
for (int i=0; i<5; i++){
pool.submit(runnable);
}
//4、關閉執行緒池
pool.shutdown();
}
補充:
shutdown:啟動有序關閉,其中先前提交的任務將被執行,但不會接受任何新任務
shutdownNow:嘗試停止所有正在執行的任務,停止等待任務的處理,並返回正在等待執行的任務列表。
execute() 和 submit() 的區別:
1)引數:execute 只能傳遞 Runnable;submit 既可以傳遞 Runnable,也可以傳遞 Callable
2)返回值:execute 沒有返回值;submit 有返回值,可以獲取 Callable 的返回結果
六、執行緒池底層原始碼檢視
newFixedThreadPool
newCachedThreadPool
newSingleThreadExecutor
newScheduledThreadPool
七、執行緒池7大引數
ThreadPoolExecutor 的底層原始碼:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
corePoolSize:執行緒池中的常駐核心執行緒數
maximumPoolSize:執行緒池中能夠容納同時指向的最大執行緒數,此值必須大於等於1
keepAliveTime:多餘空閒執行緒的存活時間
(若執行緒池中當前執行緒數超過corePoolSize時,且空閒執行緒的空閒時間達到keepAliveTime時,多餘空閒執行緒會被銷燬,直到只剩下corePoolSize個執行緒為止)
TimeUnit:keepAliveTime 的時間單位
workQueue:任務佇列,被提交但尚未被執行的任務
ThreadFactory:執行緒工廠,用於建立執行緒,一般用預設的即可
RejectedExecutionHandler:拒絕策略,當任務太多來不及處理,如何拒絕任務
八、執行緒池底層工作原理
1)執行緒池剛建立時,裡面沒有一個執行緒。任務佇列是作為引數傳進來的。不過,就算佇列裡面有任務,執行緒池也不會馬上執行它們。
2)當呼叫 execute() 方法新增一個任務時,執行緒池會做如下判斷:
a) 如果正在執行的執行緒數量小於 corePoolSize,那麼馬上建立執行緒執行這個任務;
b) 如果正在執行的執行緒數量大於或等於 corePoolSize,那麼將這個任務放入佇列;
c) 如果這時候佇列滿了,而且正在執行的執行緒數量小於 maximumPoolSize,那麼還是要建立非核心執行緒立刻執行這個任務;
d) 如果佇列滿了,而且正在執行的執行緒數量大於或等於 maximumPoolSize,那麼執行緒池會執行拒絕策略。
3)當一個執行緒完成任務時,它會從佇列中取下一個任務來執行。
4)當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,如果當前執行的執行緒數大於 corePoolSize,那麼這個執行緒就被停掉。所以執行緒池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。
九、執行緒池的4大拒絕策略
執行緒池中的執行緒已經用完了,無法繼續為新任務服務,同時,等待佇列也已經排滿了,再也塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。
JDK 內建的拒絕策略如下:
1)AbortPolicy : 直接丟擲 RejectedExecutionException 異常,阻止系統正常執行。
2)CallerRunsPolicy : 該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫執行緒。
3)DiscardOldestPolicy : 丟棄佇列中等待最久的執行緒,然後把當前任務加入佇列中嘗試再次提交當前任務。
4)DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟失,這是最好的一種方案。
以上內建拒絕策略均實現了 RejectedExecutionHandler 介面,若以上策略仍無法滿足實際需要,完全可以自己擴充套件 RejectedExecutionHandler 介面。
十、執行緒池的實際使用
通過檢視 Executors 提供的預設執行緒池的底層原始碼後,我們會發現其有如下弊端:
1)FixedThreadPool 和 SingleThreadPool:
允許的請求佇列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允許的建立執行緒數量為 Integer.MAX_VALUE,可能會堆積大量的執行緒,從而導致 OOM。
並且在《阿里巴巴Java開發手冊》中也有指出,執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方式手動建立,這樣的處理方式能讓程式設計師更加明確執行緒池的允許規則,從而規避資源耗盡的風險。
小結:在實際開發中不會使用 Executors 建立,而是手動建立,自己指定引數。
十一、執行緒池的手動建立
以上的引數是隨手寫的,實際開發中引數的設定要根據業務場景以及伺服器配置來進行設定。
十二、執行緒池配置合理執行緒數
設定執行緒池的引數時,需要從以下 2 個方面進行考慮:
系統是 CPU 密集型?
系統是 IO 密集型?
(一)CPU 密集型
CPU 密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行。
那麼這種情況下,應該儘可能配置少的執行緒數量,從而減少執行緒之間的切換,讓其充分利用時間進行計算。
一般公式為:CPU核數 + 1 個執行緒的執行緒池。
可以通過以下程式碼來檢視伺服器的核數:
Runtime.getRuntime().availableProcessors()
(二)IO 密集型
IO 密集型的意思是該任務需要大量的 IO,即大量的阻塞。
那麼這種情況下會導致有大量的 CPU 算力浪費在等待上,所以需要多配置執行緒數。
在 IO 密集型情況下,瞭解到有兩種配置執行緒數的公式:
公式一:CPU核數/(1-阻塞係數),其中阻塞係數在 0.8-0.9 之間
如:8核CPU,可以設定為 8/(1-0.9)=80 個執行緒
公式二:CPU核數 * 2
執行緒數的設定參考文章:
Java新手,若有錯誤,歡迎指正!