一、線程池概述
線程經常用來同時處理一個程序的多個任務,但是在併發任務非常多並且處理時間短的情況下,使用線程就需要面臨一個問題,假設我們把線程創建的時間看做T1,線程執行任務的時間看做T2,線程銷燬的時間看做T3,學過小學數學的都知道,只有當T2的時間足夠大時,這個線程才能執行更多的任務,而不是把時間花費在創建和銷燬上。
然而實際開發中,很少能人爲控制T2的時間,因此,如何縮減T1和T3的時間,就是目前的解決思路,也就是線程池對線程資源開銷的解決思路。
線程池將一個或者多個線程多次反覆利用,同時需要對併發任務進行處理(也就是在任務增加的情況下,動態的創建新的線程),同時也需要考慮在程序空閒的情況下,對資源的釋放(也就是在沒有新的任務的情況下,銷燬已有的線程,釋放佔用的資源)。
二、線程池構造
通常我們通過Java提供的ThreadPoolExecutor類來配置一個線程池對象。
這個類重要的構造參數主要有以下幾個:
1)corePoolSize:核心線程數
該參數決定線程池能夠一直活躍的線程數量。每當一個任務交給線程池,只要當前核心線程數小於corePoolSize,那麼就會新建一個線程運行新來的任務。
2)BlockingQueue<Runnable> workQueue:等待隊列
隊列一般分爲有界隊列和無界隊列(無界隊列因爲會無限擴容的原因,一旦處理速度跟不上任務產生速度,任務堆積,就會導致內存膨脹溢出)。
當核心線程數已經達到了設定大小時,新來的任務會交給等待隊列。
3)maximumPoolSize:最大線程數
當等待隊列也滿了的時候,線程池會進入996加班模式,也就是會繼續new Thread來處理新來的任務(如果設置的maximumPoolSize==corePoolSize,那麼這個線程池不會創建新的線程。)
4)RejectedExecutionHandler handler:拒絕執行策略
當一個任務被拒絕執行時(比如線程池和等待隊列都爆滿的情況下),會調用handler的rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法(如果有的話)。
5)keepAliveTime:保持活躍時間
這個參數主要用來限制非核心線程的存活時間。 確保不會有線程長時間進入空閒狀態,佔用資源。
但是該參數對核心線程無效,核心線程一旦被創建,除非人爲關閉,否則一直存活。
6)TimeUnit timeUnit:keepAliveTime的單位
該參數是枚舉類型,用來指定keepAliveTime的時間單位(秒,毫秒……),可以賦值TimeUnit.SECONDS表示秒。
7)ThreadFactory threadFactory:線程工廠
顧名思義,該對象主要用來創建線程對象。(JDK有提供默認的線程創建模式。)
三、線程池的創建
在二的基礎之上,我們就可以開始構造適用於自己的線程池了。
爲了演示線程創建和拒絕策略的執行,其中ThreadFactory ,RejectedExecutionHandler的實現需要自己手寫一下。
在這裏我先寫好一個任務,繼承了Runable接口
//因爲是內部類所以加了static,如果不寫成內部類,就不需要寫
static class MyTask implements Runnable {
private String taskName;
public MyTask(String taskName) {
this.taskName = taskName;
System.out.println(getTaskName() + "is create");
}
@Override
public void run() {
System.out.println(getTaskName() + "is running");
try {
Thread.sleep(2500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getTaskName() + "is over");
}
public String getTaskName() {
return "任務"+taskName;
}
@Override
public String toString() {
return "任務"+taskName;
}
}
接着實現一下線程池中ThreadFactory的創建
static class MyThreadFactory implements ThreadFactory {
private int threadNo = 0;
@Override
public Thread newThread(Runnable r) {
//線程池會調用該方法來創建線程對象
++threadNo;
System.out.println("Thread" + threadNo + "is created");
return new Thread(r,String.valueOf(threadNo));
}
}
接着準備一下拒絕執行策略
static class MyRejectedHandler implements RejectedExecutionHandler {
//線程池在拒絕一個任務的請求之後,會調用該方法
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
MyTask mt = (MyTask) r;
//獲取此時的隊列
BlockingQueue<Runnable> workQueue = executor.getQueue();
System.err.print(mt.getTaskName() + " is rejected,原因:");
System.err.print("等待隊列當前大小:"+workQueue.size() + "已經達到最大值,");
//獲取迭代器循環遍歷所有的任務,取出任務名並且打印
Iterator<Runnable> iterator = workQueue.iterator();
String message = "";
while(iterator.hasNext()){
message += iterator.next().toString()+" ";
}
System.err.println("等待隊列的任務是"+message);
}
}
現在萬事俱備,可以創建線程池了
public static void main(String[] args) {
//核心線程數
int corePoolSize = 2;
//最大線程數
int maximumPoolSize = 4;
//保持生命時間:爲了節約資源,當一個線程空閒時間超過keepAliveTime時,該線程會被註銷(默認對核心線程無效)
long keepAliveTime = 2;
//keepAliveTime的時間單位,屬於枚舉類型
TimeUnit timeUnit = TimeUnit.SECONDS;
//等待隊列:構造函數表示隊列的大小,當所有的核心線程都在忙碌時,新來的任務會被存儲在此
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
//線程創建工廠:工廠設計模式,用來構建線程對象
ThreadFactory threadFactory = new MyThreadFactory();
//拒絕執行後處理器:當等待隊列已滿,新來的任務會被拒絕執行,此時會調用處理器的rejectedExecution方法來處理異常
RejectedExecutionHandler handler = new MyRejectedHandler();
//創建線程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, threadFactory, handler);
//創建核心線程
int coreNo = threadPoolExecutor.prestartAllCoreThreads();
System.out.println(coreNo+"個核心線程被創建");
//循環創建任務
for (int i=1;i<=10;i++){
threadPoolExecutor.execute(new MyTask(String.valueOf(i)));
}
//程序執行之後不會自己停止,因爲線程池還掛着呢。需要手動釋放線程才能結束程序。
}
最後看一下運行結果吧。
可以看到,一開始手動創建了所有的核心線程(空閒狀態),之後開始循環創建任務。
接着任務1和任務2因爲此時核心線程空閒,直接運行,任務3和任務4則在等待隊列中等待。
之後又創建了任務5,此時核心線程和等待隊列已經滿了,但是線程數還沒有達到最大,因此創建了新的線程3來執行這個新任務。
任務6和任務5的待遇相同。因爲線程的創建需要時間,因此任務的運行和創建之間等待的時間較長。
任務7,8,9,10因爲此時的線程數已經達到最大,等待隊列也爆滿,因此,全部都被拒絕執行並且執行了拒絕策略。
接着,任務1和任務2執行完畢,等待隊列的任務3和任務4繼續執行。
最後所有的任務都執行完畢,程序……還沒停。
需要手動銷燬線程池之後,程序纔會正常關閉
四、線程池的關閉
一般情況下,線程池都是爲了解決併發項目的任務而開啓的,所以並不需要關閉線程池。但是如果想要關閉線程池的話,由於核心線程並不會自己銷燬,因此需要手動結束線程,線程池想要自動銷燬的話,需要滿足兩個條件。
- 線程池引用不可達。
- 線程池的線程數爲0。
推薦通過調用線程池提供的方法來關閉線程。
主要有以下幾個方法:
- shutdown():該方法會準備關閉線程池,執行此方法,會等待所有任務結束之後,再關閉線程池。
- shutdownNow():該方法會立刻關閉線程池,執行此方法,正在執行的任務會被強制中斷。
- getActiveCount():該方法會返回當前正在工作的線程數,當所有的線程都處於空閒狀態時,返回值爲0。
當線程池被關閉之後,線程池將無法再執行新的任務,否則會拋出異常!(準確來說,會執行拒絕執行策略代碼)。
五、線程關閉機制
雖然在之前提到,使用shutdownNow()可以立即關閉線程池,但是其實質上並不是如此。
追蹤源碼我們可以發現,線程池的shutdownNow()方法,本質上還是調用了線程本身的關閉方法interrupt()。所以,shutdownNow()方法也不能保證立即關閉線程池。
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
//shutdownNow()的底層實現是調用了線程對象的interrupt方法
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
因爲線程的關閉方法interrupt() 其本質上並沒有關閉線程,能夠直接關閉線程的stop()等方法都因爲線程安全問題而廢棄。interrupt()方法僅僅只是更改了線程對象的一個標誌位,告訴這個線程,應該要關閉了,但是實際上是否關閉,取決於線程本身。因此,即便使用showdownNow()方法之後,線程池中的線程關閉還是不關閉,也要取決於線程自身。。。
這裏根據應用場景有多個不同解決方案,如果任務是經常做循環或者遞歸操作,那麼可以用這樣的方式:
@Override
public void run() {
for (int i=0;i<1000000;i++){
//在方法體中添加校驗關閉標誌位,判斷線程是否應該關閉
if(Thread.interrupted()){
System.out.println("退出線程"+i);
break;
}else{
i++;
}
}
}
這個程序在使用線程池運行,並且使用shutdownNow()關閉後,結果如下
可以發現,該任務的循環並沒有結束,線程就被關閉了。
如果去掉校驗,改成這樣:
@Override
public void run() {
int i = 0;
for (;i<1000000;i++){
//在方法體中去掉標誌位
i++;
}
System.out.println("退出線程"+i);
}
運行結果:
即便使用了shutdownNow(),線程依舊做完了循環才關閉,這樣看來在多次循環中如果不對標誌位認爲判斷,shutdownNow()和shutdown()幾乎沒什麼區別了。