多執行緒的程式的確能發揮多核處理器的效能。雖然與程式相比,執行緒輕量化了很多,但是其建立和關閉同樣需要花費時間。而且執行緒多了以後,也會搶佔記憶體資源。如果不對執行緒加以管理的話,是一個非常大的隱患。而執行緒池的目的就是管理執行緒。當你需要一個執行緒時,你就可以拿一個空閒執行緒去執行任務,當任務執行完後,執行緒又會歸還到執行緒池。這樣就有效的避免了重複建立、關閉執行緒和執行緒數量過多帶來的問題。
Java併發包提供的執行緒池
注:摘自《實戰Java高併發程式設計》
如圖是Java併發包下提供的執行緒池功能。其中ExecutorService介面提供一些操作執行緒池的方法。而Executors相當於一個執行緒池工廠類,它裡面有幾種現成的具備某種特定功能的執行緒池工廠方法。看到這些應該不陌生,舉個我們平時最常使用的例子:
//建立一個大小為10的固定執行緒池 ExecutorService threadpool= Executors.newScheduledThreadPool(10);
下面簡單介紹一下這些工廠方法:
newFixedThreadPool()方法:固定執行緒數量執行緒池。傳入的數字就是執行緒的數量,如果有空閒執行緒就去執行任務,如果沒有空閒執行緒就會把任務放到一個任務佇列,等到有執行緒空閒時便去處理佇列中的任務。
newSingleThreadExecutor()方法:只有一個執行緒的執行緒池。同樣,超出的任務會被放到任務佇列,等這個執行緒空閒時就會去按順序處理。
newCachedThreadPool()方法:可以根據實際情況擴充的執行緒池。當沒有空閒執行緒去執行新任務時,就會再建立新的執行緒去執行任務,執行完後新建的執行緒也會返回執行緒池進行復用。
newSingleThreadScheduledExecutor()方法:返回的是ScheduledExecutorService物件。ScheduledExecutorService是繼承於ExecutorService的,有一些擴充方法,如指定執行時間。這個執行緒池大小為1,在指定時間執行任務。關於指定時間的幾個方法:schedule()是在指定時間後執行一次任務。scheduleAtFixedRate()和方法scheduleWithFixedDelay()方法,兩者都是週期性的執行任務,但是前者是以上一次任務開始為週期起點,後者是以上一次任務結束為週期起點。具體的引數大家可以在IDE裡面檢視。
newScheduledThreadPool()方法:和上面一個方法一樣,但是可以指定執行緒池大小,其實上面那個方法也是呼叫這個方法的,只是傳入的引數是1。
執行緒池核心類
上面簡單的對Java併發包下執行緒池的結構和API進行簡單的介紹,下面開始深入瞭解一下執行緒池。如果大家在IDE上追蹤一下上面幾個工廠方法就會發現,其中最後都會呼叫一個方法,通過上圖其實也可以發現。那就是ThreadPoolExecutor的構造方法,工廠方法只是幫我們傳入不同的引數,從而實現不同的效果,所以如果你想更自由的控制自己的執行緒池,推薦直接使用ThreadPoolExecutor建立執行緒池。下面給出這個建構函式的引數列表:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
引數從上到下,作用依次為:
1.指定執行緒池種執行緒的數量。
2.執行緒池種最大的執行緒數量,也就是最大能擴充到多少。
3.當執行緒數量超過corePoolSize,多餘的空閒執行緒多久會被銷燬。
4.keepAliveTime的單位。
5.任務佇列,當空閒執行緒不夠,也不能再新建執行緒時,新提交的任務就會被放到任務佇列種。
6.執行緒工廠,用於建立執行緒,預設的即可。
7.拒絕策略。當任務太多,達到最大執行緒數量、任務佇列也滿了,該如何拒絕新提交的任務。
任務佇列
任務佇列是一個BlockingQueue介面,在ThreadPoolExecutor一共有如下幾種實現類實現了BlockingQueue介面。
SynchronousQueue:直接提交佇列。這種佇列其實不會真正的去儲存任務,每提交一個任務就直接讓空閒執行緒執行,如果沒有空閒執行緒就去新建,當達到最大執行緒數時,就會執行拒絕策略。所以使用這種任務佇列時,一般會設定很大的maximumPoolSize,不然很容易就執行了拒絕策略。newCachedThreadPool執行緒池的corePoolSize為0,maximumPoolSize無限大,它用的就是直接提交佇列。
ArrayBlockingQueue:有界任務佇列,其建構函式必須帶一個容量引數,表示任務佇列的大小。當執行緒數量小於corePoolSize時,有任務進來優先建立執行緒。當執行緒數等於corePoolSize時,新任務就會進入任務佇列,當任務佇列滿了,才會建立新執行緒,執行緒數達到maximumPoolSize時執行拒絕策略。
LinkedBlockingQueue:無界任務佇列,通過它的名字也應該知道了,它是個連結串列,除非沒有空間了,不然不會出現任務佇列滿了的情況,但是非常耗費系統資源。和有界任務佇列一樣,執行緒數若小於corePoolSize,新任務進來時沒有空閒執行緒的話就會建立新執行緒,當達到corePoolSize時,就會進入任務佇列。會發現沒有maximumPoolSize什麼事,newFixedThreadPool固定大小執行緒池就是用的這個任務佇列,它的corePoolSize和maximumPoolSize相等。
PriorityBlockingQueue:優先任務佇列,它是一個特殊的無界佇列,因為它總能保證高優先順序的任務先執行。
拒絕策略
JDK提供了四種拒絕策略。
AbortPolicy:直接丟擲異常,阻止系統正常工作。
CallerRunsPolicy:如果執行緒池未關閉,則在呼叫者執行緒裡面執行被丟棄的任務,這個策略不是真正的拒絕任務。比如我們在T1執行緒中提交的任務,那麼該拒絕策略就會把多餘的任務放到T1執行緒執行,會影響到提交者執行緒的效能。
DiscardOldestPolicy:該策略會丟棄一個最老的任務,也就是即將被執行的任務,然後再次嘗試提交該任務。
DiscardPolicy:直接丟棄多餘的任務,不做任何處理,如果允許丟棄任務,這個策略是最好的。
以上內建的拒絕策略都實現了RejectedExecutionHandler介面,所以上面的拒絕策略無法滿足你的要求,可以自定義一個:繼承RejectedExecutionHandler並實現rejectedExecution方法。
執行緒工廠
執行緒池中的執行緒是由ThreadFactory負責建立的,一般情況下預設就行,如果有一些其他的需求,比如自定義執行緒的名稱、優先順序等,我們也可以利用ThreadFactory介面來自定義自己的執行緒工廠:繼承ThreadFactory並實現newThread方法。
執行緒池的擴充
在ThreadPoolExecutor中有三個擴充套件方法:分別會在任務執行前beforeExecute、執行完成afterExecute、執行緒池退出時執行terminated。
這幾個方法在哪呼叫的?在ThreadPoolExecutor中有一個內部類:Worker,每個執行緒的任務其實都是由這個類裡面的run方法執行的,貼一下這個類的原始碼:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable { /** * This class will never be serialized, but we provide a * serialVersionUID to suppress a javac warning. */ private static final long serialVersionUID = 6138294804551838833L; /** Thread this worker is running in. Null if factory fails. */ final Thread thread; /** Initial task to run. Possibly null. */ Runnable firstTask; /** Per-thread task counter */ volatile long completedTasks; //....省略 /** Delegates main run loop to outer runWorker */ public void run() { runWorker(this); } //....省略 }
接著進入這個runWorker方法:
final void runWorker(Worker w) { //...省略 try { //任務執行前 beforeExecute(wt, task); Throwable thrown = null; try { task.run(); } catch (RuntimeException x) { thrown = x; throw x; } catch (Error x) { thrown = x; throw x; } catch (Throwable x) { thrown = x; throw new Error(x); } finally { //任務執行完 afterExecute(task, thrown); } } //....省略 }
還有一個執行緒池退出時執行的方法是在何處執行的?這個方法被呼叫的地方就不止一處了,像執行緒池的shutdown方法就會呼叫
public void shutdown() { //....省略。這個方法裡面就會呼叫terminated tryTerminate(); }
ThreadPoolExecutor中這三個方法預設是沒有任何內容的,所以我們要自定義它也很簡單,直接重寫它們就行了:
ExecutorService threadpool= new ThreadPoolExecutor(5,5,0L,TimeUnit.SECONDS,new LinkedBlockingDeque<>()){ @Override protected void beforeExecute(Thread t, Runnable r) { //執行任務前 } @Override protected void afterExecute(Runnable r, Throwable t) { //執行任務後 } @Override protected void terminated() { //執行緒退出 } };