多線程的異步執行方式,雖然能夠最大限度發揮多核計算機的計算能力,但是如果不加控制,反而會對系統造成負擔。線程本身也要佔用內存空間,大量的線程會佔用內存資源並且可能會導致Out of Memory。即便沒有這樣的情況,大量的線程回收也會給GC帶來很大的壓力。爲了避免重複的創建線程,線程池的出現可以讓線程進行復用。通俗點講,當有工作來,就會向線程池拿一個線程,當工作完成後,並不是直接關閉線程,而是將這個線程歸還給線程池供其他任務使用。
線程池整體結構
Executor是線程池最上層的接口,這個接口有一個核心方法execute(Runnable command),具體是由ThreadPoolExecutor類實現,用於執行任務,ExecutorService接口繼承Executor,提供了shutdown(),shutdownNew(),submit()等用來關閉和執行線程的方法。
ExecutorService最終的默認實現類ThreadPoolExecutor。
ThreadPoolExecutor分析
首先通過構造器來一步一步分析。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:線程池的核心池大小,在創建線程池之後,線程池默認沒有任何線程,創建之後,默認線程池線程數量爲0,當任務過來時就會創建一個新的線程,直到達到corePoolSize之後,會將線程放入workQueue中
-
maximumPoolSize:最大線程數量
-
workQueue一個阻塞隊列,用來存儲等待執行的任務,當線程池中的線程數超過它的corePoolSize的時候,線程會進入阻塞隊列。通過workQueue,線程池實現了阻塞功能
-
threadFactory:線程工廠,用來創建線程
-
handler:表示當拒絕處理任務時的策略
-
keepAliveTime:線程池維護線程鎖允許的空閒時間,當線程池中的線程數量大於corePoolSize的時候,如果這時沒有新的任務提交,核心線程之外的線程不會立即銷燬,而是會等待,直到等待的時間超過了keepAliveTime
這些參數是怎麼再具體執行中發揮作用的呢,我們來看下
execute的執行流程圖
當任務過來時就會創建一個新的線程,直到達到corePoolSize之後,會將線程放入workQueue中, 如果運行線程小於corePoolSize,即使有空閒線程,當任務過來時也會創建新的線程 如果線程池數量大於corePoolSize但小於maximumPoolSize,則只有當workQueue滿時才創建新的線程去處理任務
如果運行線程數量大於等於maximumPoolSize時,這時如果workQueue也已經滿了,則通過handler所指定的策略去處理,如果除核心線程之外的線程沒有處理任務,且超過keepAliveTime,就會被回收。
四種常用線程池
01 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
-
工作線程的創建數量幾乎沒有限制(其實也有限制的,數目爲Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
-
如果長時間沒有往線程池中提交任務,即如果工作線程空閒了指定的時間(默認爲1分鐘),則該工作線程將自動終止。終止後,如果你又提交了新的任務,則線程池重新創建一個工作線程。
-
在使用CachedThreadPool時,一定要注意控制任務的數量,否則,由於大量線程同時運行,很有會造成系統癱瘓。
02 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到線程池池隊列中。
-
FixedThreadPool是一個典型且優秀的線程池,它具有線程池提高程序效率和節省創建線程時所耗的開銷的優點。但是,在線程池空閒時,即線程池中沒有可運行任務時,它不會釋放工作線程,還會佔用一定的系統資源。
03 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
一個單線程的線程池,只有一個線程在工作(如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。)能夠保證所有任務的執行順序按照任務的提交順序執行,同一時段只有一個任務在運行。
04 newScheduleThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
返回值是ScheduledExecutorService
創建一個定長的線程池,而且支持定時的以及週期性的任務執行。
可定時運行(初始延時),運行頻率(每隔多長時間運行,還是運行成功一次之後再隔多長時間再運行)的線程池
適合定時以及週期性執行任務的場合。
這裏有個執行方法需要注意的。
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 週期性執行某一個任務,線程池提供了兩種調度方式,這裏單獨演示一下。測試場景一樣。
// 測試場景:提交的任務需要3秒才能執行完畢。看兩種不同調度方式的區別
// 效果1: 提交後,2秒後開始第一次執行,之後每間隔1秒,固定執行一次(如果發現上次執行還未完畢,則等待完畢,完畢後立刻執行)。
// 也就是說這個代碼中是,3秒鐘執行一次(計算方式:每次執行三秒,間隔時間1秒,執行結束後馬上開始下一次執行,無需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務-1 被執行,現在時間:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
// 效果2:提交後,2秒後開始第一次執行,之後每間隔1秒,固定執行一次(如果發現上次執行還未完畢,則等待完畢,等上一次執行完畢後再開始計時,等待1秒)。
// 也就是說這個代碼鐘的效果看到的是:4秒執行一次。 (計算方式:每次執行3秒,間隔時間1秒,執行完以後再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任務-2 被執行,現在時間:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}
阻塞隊列
在線程池中,如果任務數量超過了核心線程數,就會把任務放入阻塞隊列等待運行,在線程池中主要有三種阻塞隊列
-
ArrayBlockingQueue :基於數組的有界隊列。
-
LinkedBlockingQueue:基於鏈表的先進先出隊列,是無界的。
-
SynchronousQueue:無緩衝等待隊列,它將任務直接交給線程處理而不保持它們。如果不存在可用於立即運行任務的線程(即線程池中的線程都在工作),則試圖把任務加入緩衝隊列將會失敗,因此會構造一個新的線程來處理新添加的任務,並將其加入到線程池中。
四種拒絕策略
-
AbortPolicy 丟棄任務,並拋出RejectedExecutionException 異常
-
CallerRunsPolicy:該任務被線程池拒絕,由調用 execute方法的線程執行該任務。
-
DiscardOldestPolicy :拋棄隊列最前面的任務,然後重新嘗試執行任務。
-
DiscardPolicy:丟棄任務,不過也不拋出異常。