線程池
定義
1. 線程池是什麼?
線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。
線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然後在創建線程後自動啓動這些任務。
多線程的異步執行方式,雖然能夠最大限度發揮多核計算機的計算能力,但是如果不加控制,反而會對系統造成負擔。線程本身也要佔用內存空間,大量的線程會佔用內存資源並且可能會導致Out of Memory。即便沒有這樣的情況,大量的線程回收也會給GC帶來很大的壓力。
爲了避免重複的創建線程,線程池的出現可以讓線程進行復用。通俗點講,當有工作來,就會向線程池拿一個線程,當工作完成後,並不是直接關閉線程,而是將這個線程歸還給線程池供其他任務使用。
使用
1. 線程池創建
線程池概念來源於Java中的Executor,它是一個接口,還有一個子類接口ExecutorService,一個抽象類AbstractExecutorService,真正的實現類爲ThreadPoolExecutor。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.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
下面解釋這些參數的含義:
1. corePoolSize:
核心池的大小,這個參數跟後面講述的線程池的實現原理有非常大的關係。在創建了線程池後,默認情況下,線程池中並沒有任何線程,而是等待有任務到來才創建線程去執行任務,除非調用了prestartAllCoreThreads()或者prestartCoreThread()方法,從這2個方法的名字就可以看出,是預創建線程的意思,即在沒有任務到來之前就創建corePoolSize個線程或者一個線程。默認情況下,在創建了線程池後,線程池中的線程數爲0,當有任務來之後,就會創建一個線程去執行任務,當線程池中的線程數目達到corePoolSize後,就會把到達的任務放到緩存隊列當中。
2. maximumPoolSize:
線程池最大線程數,這個參數也是一個非常重要的參數,它表示在線程池中最多能創建多少個線程。
3. keepAliveTime: 表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,即當線程池中的線程數大於corePoolSize時,如果一個線程空閒的時間達到keepAliveTime,則會終止,直到線程池中的線程數不超過corePoolSize。但是如果調用了allowCoreThreadTimeOut(boolean)方法,在線程池中的線程數不大於corePoolSize時,keepAliveTime參數也會起作用,直到線程池中的線程數爲0。
3. unit:
TimeUnit枚舉類型的值,代表keepAliveTime時間單位,可以取下列值:
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小時
TimeUnit.MINUTES; //分鐘
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //納秒
4. workQueue: 一個阻塞隊列,用來存儲等待執行的任務,這個參數的選擇也很重要,會對線程池的運行過程產生重大影響,一般來說,這裏的阻塞隊列有以下幾種選擇
ArrayBlockingQueue;
LinkedBlockingQueue;
SynchronousQueue;
ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。
5. threadFactory: 線程工廠,是用來創建線程的。默認new Executors.DefaultThreadFactory();
6. handler: 線程拒絕策略。當創建的線程超出maximumPoolSize,且緩衝隊列已滿時,新任務會拒絕,有以下取值:
ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務
Java通過Executors工廠類提供四種線程池,分別爲:
- newCachedThreadPool
可緩存線程池,先查看池中有沒有以前建立的線程,如果有,就直接使用。如果沒有,就建一個新的線程加入池中,緩存型池子通常用於執行一些生存期很短的異步型任務
// 可緩存線程池
ExecutorService threadPool = Executors.newCachedThreadPool();
threadPool.execute(mTestRunnable);
- newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
// 創建一個可重用固定個數的線程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.execute(mTestRunnable);
- newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
// 定長線程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
println("定時執行");
threadPool.schedule(mTestRunnable, 1, TimeUnit.SECONDS);
- newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
例如:
public static void main(String[] args) {
//
ExecutorService threadPool=Executors.newSingleThreadExecutor();
threadPool.execute(mTestRunnable);
}
private static Runnable mTestRunnable=new Runnable() {
@Override
public void run() {
println("測試線程池-----");
}
};
可以看到線程池的使用還是很簡單的。關鍵還是要搞清楚其實現原理。
2. 爲什麼阿里巴巴開發手冊不推薦使用Executors提供的靜態工廠方法創建線程池?
3. 線程池的關閉
ThreadPoolExecutor提供了兩個方法,用於線程池的關閉,分別是shutdown()和shutdownNow(),其中:
shutdown():不會立即終止線程池,而是要等所有任務緩存隊列中的任務都執行完後才終止,但再也不會接受新的任務
shutdownNow():立即終止線程池,並嘗試打斷正在執行的任務,並且清空任務緩存隊列,返回尚未執行的任務
原理
1. 線程池是怎麼實現複用線程的?
線程池中長期駐留了一定數量的活線程,當任務需要執行時,我們不必先去創建線程,線程池會自己選擇利用現有的活線程來處理任務。
很顯然,線程池一個很顯著的特徵就是“長期駐留了一定數量的活線程”,避免了頻繁創建線程和銷燬線程的開銷,那麼它是如何做到的呢?我們知道一個線程只要執行完了run()方法內的代碼,這個線程的使命就完成了,等待它的就是銷燬。既然這是個“活線程”,自然是不能很快就銷燬的
在分析源碼之前先來思考一下要怎麼去分析,源碼往往是比較複雜的,如果知識儲備不夠豐厚,很有可能會讀不下去,或者讀岔了。一般來講要時刻緊跟着自己的目標來看代碼,跟目標關係不大的代碼可以不理會它,一些異常的處理也可以暫不理會,先看正常的流程。就我們現在要分析的源碼而言,目標就是看看線程是如何被複用的
先重構造函數看起:
ThreadPoolExecutor這個是線程池的實現類。裏面的字段含義上面已經解釋過了。這裏再簡單回顧下
- corePoolSize 核心線程數
- maximumPoolSize 最大線程數
- workqueue 工作隊列-其實就是任務隊列
- keepAliveTime 非核心線程存活的最大時間
- threadFactory 線程創建工廠類
- handler 異常處理類
比較難理解的就是workqueue 其實它是BlockingQueue 這個是阻塞隊列。從隊列中讀取數據如果隊列爲空,那麼此線程會掛起等待 直到讀取數據。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
字段ctl是對線程池的運行狀態和線程池中有效線程的數量進行控制的, 它包含兩部分信息: 線程池的運行狀態 (runState) 和線程池內有效線程的數量 (workerCount)。這裏是採用的位運算。AtomicInteger是保證原子操作。大家可以運行看看結果:
接着看execute方法:
我們先看紅色框的判斷,如果當前工作線程數量小於核心數量,則去添加工作線程。那我們看addWorker.
首先這裏有個retry:這相當於一個goto語句。正常 break和continue就退出了,但是這個就是退到 retry處 開始。可以簡單寫一個測試一下。
首先檢測線程池運行狀態。
然後檢測線程池中的工作線程數量
這裏用了CAS保證線程同步問題。繼續往下看
創建一個工作線程Worker 並且把任務傳遞進去。
ReentrantLock鎖 同步操作
檢測運行狀態
把工作線程添加進入workers.
這個workers就是線程池中的池 存儲工作線程。數據結構是hashset。
然後調用start啓動工作線程。
接着就進入了工作線程的run方法。這個方法其實就是worker類中的run方法。
其實只要英語好的,值看這個方法的註釋就知道這個方法在幹嘛了。
這裏開啓了一個無限循環處理任務。
如果task不爲空就直接 回調 task的run方法。
如果爲空則去隊列中取出getTask();
紅框說的很清楚,執行阻塞 或者 定時等待任務 或者返回null。
如果返回null 那麼循環就退出了,線程就算是銷燬了。阻塞就會一直等待任務。
關鍵就是這個 poll和take 方法是不同的。take會一直阻塞,poll是定時。
到此爲止,其實原理已經比較清楚了。池就是hashset。裏面維護了一些 線程 他們的run方法中 開啓無線循環 從阻塞隊列中取任務,有就執行,沒有就阻塞。execute方法就根據當前的情況是 創建新的工作線程,還是往隊列中添加任務。
2. execute和submit有什麼區別?
submit方法是ExecutorService接口中定義的。execute方法是Executor接口中定義的。ExecutorService是Executor的子類
submit方法的實現在AbstractExecutorService抽象類中。
和execute 區別不大,多了結果回調。對應上面線程創建的第三種方式。
總結
-
所謂線程池本質是一個hashSet。多餘的任務會放在阻塞隊列中。
-
只有當阻塞隊列滿了後,纔會觸發非核心線程的創建。所以非核心線程只是臨時過來打雜的。直到空閒了,然後自己關閉了。
-
線程池提供了兩個鉤子(beforeExecute,afterExecute)給我們,我們繼承線程池,在執行任務前後做一些事情。
-
線程池原理關鍵技術:鎖(lock,cas)、阻塞隊列、hashSet(資源池)、位運算、同步