多線程【線程池】
一、什麼是線程池
線程池:指在初始化一個多線程應用程序過程中創建一個線程集合,然後在需要執行新的任務時重用這些線程而不是新建一個線程,
一旦任務已經完成了,線程回到池子中並等待下一次分配任務。
二、使用線程池的好處
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 進行任務操作(並行操作),所以適合使用在很耗時的任務中。
參考文章:
//blog.csdn.net/qq_38428623/article/details/86689800
//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新手,若有錯誤,歡迎指正!