線程池的優點
- 線程是稀缺資源,使用線程池可以減少創建銷燬線程的次數,每個工作線程都可以重複使用。
- 可以根據系統的承受能力,調整線程池中工作線程的數量,防止因爲消耗過多內存導致服務器崩潰。
線程池的創建
創建時,有多個構造方法,參數個數不同,最終都調用下面的構造方法進行創建。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
參數含義:
- corePoolSize:線程池核心線程數量
- maximumPoolSize:線程池最大線程數量
- keepAliveTime:當活躍線程數大於核心線程數時,空閒的多餘線程的最大存活時間
- unit:存活時間的單位
- workQueue:存放任務的隊列
常用的workQueue類型:
- SynchronousQueue:這個隊列接收到任務的時候,會直接提交給線程處理,而不保留它,如果所有線程都在工作怎麼辦?那就新建一個線程來處理這個任務!所以爲了保證不出現<線程數達到了maximumPoolSize而不能新建線程>的錯誤,使用這個類型隊列的時候,maximumPoolSize一般指定成Integer.MAX_VALUE,即無限大。
- LinkedBlockingQueue:這個隊列接收到任務的時候,如果當前線程小於核心線程數,則新建線程(核心線程)處理任務,如果當前線程數等於核心線程數,則進入隊列等待調度。由於這個隊列默認設置容量爲
Integer.MAX_VALUE
,即超過核心線程數的任務都將加載到隊列中,這個隊列會導致maximumPoolSize參數設置失效,因爲線程池中線程數永遠等於corePoolSize。 - ArrayBlockingQueue:可以限定隊列的長度,接收到任務的時候,如果沒有達到corePoolSize值,則新建核心線程執行任務,如果達到了,則入隊等待,如果隊列已滿,則新建非核心線程執行任務,如果達到了總線程數,並且隊列也滿了,則報錯。
- DelayQueue:隊列內元素必須實現Delayed接口,這就意味着你穿進去的任務必須先實現Delayed接口。這個隊列接收到任務時,首先入隊,達到指定的延時時間才執行任務,相當於延時隊列。
- threadFactory:創建線程的方式。
- handler:超出線程範圍和隊列容量的任務的處理程序
任務拒絕策略:
1、ThreadPoolExecutor.AbortPolicy
:丟棄任務並拋出RejectedExecutionException異常。
2、ThreadPoolExecutor.DiscardPolicy
:也是丟棄任務,但是不拋出異常。
3、ThreadPoolExecutor.DiscardOldestPolicy
:丟棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
4、ThreadPoolExecutor.CallerRunsPolicy
:由調用線程處理該任務
常見線程池
CachedThreadPool()
可緩存線程池,線程數無限制(Integer.MAX_VALUE
),有空閒線程則複用空閒線程,若無則新建線程
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
FixedThreadPool()
定長線程池,可控制線程最大併發數,超過線程數的任務在隊列中等待。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ScheduledThreadPool()
有時間延時的線程池,這個線程池間接的利用ThreadPoolExecutor創建。支持定時及週期性任務執行。任務隊列使用的是延時隊列,可以規定時間延遲。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
SingleThreadExecutor()
單線程線程池,核心線程和最大線程數都爲1,所有任務按指定順序執行,遵循隊列的入隊出隊規則。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
SingleThreadScheduledExecutor()
單線程可延時線程池,該線程池結合了ScheduledThreadPool和SingleThreadExecutor兩個線程池的內容。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
線程池的實現原理
提交一個任務到線程池中,線程池的處理流程如下:
1、判斷線程池中的核心線程是否都在執行任務,如果沒有則創建一個新的工作線程來執行任務。如果核心線程數已經達到創建限制,並且核心線程都在執行任務,則進行下個流程。
2、線程池判斷任務隊列是否已滿,如果任務隊列未滿,則將任務提交存儲到任務隊列中。如果任務隊列滿了,則進入下個流程。
3、判斷線程池中的線程是否都處於工作狀態,如果沒有,則創建一個工作線程(非核心線程)來執行任務,如果這時,線程都在工作且線程數達到線程池最大線程數量,則交給飽和策略來處理這個任務(handler)。
源碼分析
下面寫一個簡單例程進行源碼分析的入口,測試demo如下:
public class ThreadPoolTest {
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue);
for (int i = 0; i < 16; i++) {
poolExecutor.execute(()->{
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("線程池中活躍的線程數:"+ poolExecutor.getPoolSize());
if (queue.size() > 0){
System.out.println("-------------------任務隊列阻塞的線程數"+queue.size());
}
}
poolExecutor.shutdown();
}
}
- 首先查看線程池ThreadPoolExecutor類的execute方法,可以看到註釋內容即是線程池實現原理。具體幾步解釋見下面中文解釋。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
//判斷線程數量是否小於核心線程數
if (workerCountOf(c) < corePoolSize) {
//創建線程並啓動
if (addWorker(command, true))
return;
c = ctl.get();
}
//向任務隊列中提交任務
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
//提交成功後,進行校驗,判斷線程池是否運作中,如果線程池沒有運行中,則移除插入的任務,然後提交給飽和策略
if (! isRunning(recheck) && remove(command))
reject(command);
//判斷可用線程數是否等於0,是,開啓工作線程執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//嘗試開啓工作線程,如果失敗,則提交給飽和策略
else if (!addWorker(command, false))
reject(command);
}
- 接下來,我們查看addWorker方法,創建線程的方法。該方法有兩個參數,而第二個core參數,表示的創建的是核心線程還是非核心線程,傳參爲boolean型。
- 如下圖第一個框內,是進行判斷,判斷線程池狀態大於等於shutdown(即狀態不是運行中),並且再滿足後面的條件,則不處理直接返回。
- 第二個框中,是更新線程池線程數量,根據傳入的core 參數,判斷是否爲核心線程,如果爲true,且線程數小於corePoolSize,則跳出循環,創建核心線程。
3.接下來的代碼如下圖,第一個框中,將任務封裝到Worker類中,並獲得線程池主鎖,保證線程安全。在這裏,線程池中的線程是通過Worker類來實現的。
4.第二個框中,再次校驗線程池狀態和剛創建的線程狀態,接下來將線程加入線程池中(workes)。
5.第三個框中,啓動剛創建的線程。
- 下面我們查看Worker類的run方法,run方法中調用了runWorker方法,源碼如下圖。
- 第一個框中,當該線程是剛創建第一次執行任務,或者從任務隊列中獲取到任務,首先判斷線程池狀態,如果線程池不是停止狀態,清除當前線程的中斷標誌後再次判斷線程池狀態;如果前者判斷爲true,接下來再次判斷當前線程是否爲中斷,再結合前者條件,true:將當前線程中斷,false:執行當前線程。
- 第二個框中,執行任務開始前操作鉤子。
- 第三個框中,執行任務。
- finally塊中,執行執行任務後鉤子。
在上述簡單demo中,運行後會出現如下錯誤,因爲我們設置的LinkedBlockingQueue隊列的容量是5,核心線程爲5,最大線程數爲10,for循環將開啓總共16個線程執行16次任務。當線程池中線程數達到最大線程數10後,開始往隊列中存儲任務,存儲5個,隊滿後,線程數和隊列容量都達到了臨界點,所以執行飽和策略,由於未定義,所以執行默認的RejectedExecutionException
異常。
線程池中活躍的線程數:7
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:8
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:9
-------------------任務隊列阻塞的線程數5
線程池中活躍的線程數:10
-------------------任務隊列阻塞的線程數5
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@6d03e736[Running, pool size = 10, active threads = 10, queued tasks = 5, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at threadPools.ThreadPoolTest.main(ThreadPoolTest.java:19)
接下來,我們定義一個丟棄策略,在創建線程池時將該參數傳入,再次執行將不會出現上述異常,因爲丟棄策略是將超出限制的線程丟棄不執行。除此之外,還可以自定義其他飽和策略來處理當線程池達到飽和狀態的情況。
public class ThreadPoolTest {
public static void main(String[] args) {
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5);
//丟棄策略,多餘的丟棄,不報錯
ThreadPoolExecutor.DiscardPolicy handler = new ThreadPoolExecutor.DiscardPolicy();
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, queue,handler);
for (int i = 0; i < 16; i++) {
poolExecutor.execute(()->{
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("線程池中活躍的線程數:"+ poolExecutor.getPoolSize());
if (queue.size() > 0){
System.out.println("-------------------任務隊列阻塞的線程數"+queue.size());
}
}
poolExecutor.shutdown();
}
}