線程
在操作系統(tǒng)中,線程一般是操作系統(tǒng)能夠進行運算調(diào)度的最小單位,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務(wù),在Java中線程能力由Thread類實現(xiàn),其使用過程也十分簡單只需三步即可。
1、將需要單獨執(zhí)行的代碼放在一個類的run方法中,這個類需要實現(xiàn)Runnable接口:
class WorkTask implements Runnable {
@Override
public void run() {
// doSomething...
}
}
WorkTask workTask = new WorkTask();
如果使用的是Java8及以上版本,也可以直接使用lambda表達式創(chuàng)建:
Runnable workTask = () -> {
// doSomething
};
2、使用創(chuàng)建的Runnadble對象構(gòu)造Thread對象
Thread workThread = new Thread(workTask);
3、啟動線程
workThread.start();
另外,由于Thread類本身實現(xiàn)了Runnable接口,所以我們也可以直接集成Thread類然后復(fù)寫它的run方法來定義線程,需要注意的是,不管通過哪種方式創(chuàng)建線程,如果要啟動它,都必須調(diào)用start()方法,不能直接調(diào)用run方法,直接調(diào)用run方法會在原線程執(zhí)行其中的代碼,而不是啟動新線程。
線程的狀態(tài)
線程從創(chuàng)建出來之后會有以下幾種狀態(tài)
New(新建)
Runnable(可運行)
Blocked(阻塞)
Waiting(等待)
Timed waiting(計時等待)
Terminated(終止)
新建線程
使用new創(chuàng)建完一個Thread對象之后,該線程就處于新建狀態(tài),此時這個線程還沒有開始運行,其中的代碼也沒有被執(zhí)行。
可運行線程
當調(diào)用了start方法之后,線程就處于可運行狀態(tài),可運行意味著這個線程可能正在運行,也可能沒有運行,需要操作系統(tǒng)提供具體的運行時間,Java語言規(guī)范并沒有將運行中作為一個單獨的狀態(tài),一個正在運行的狀態(tài)仍然處于可運行狀態(tài)。
阻塞和等待線程
當線程處于阻塞或者等待狀態(tài)時,它不運行任何代碼,而且消耗最少的資源,需要由線程調(diào)度器重新激活這個線程。
當一個線程試圖獲取一個內(nèi)部的對象鎖,而這個鎖目前被其他線程占有,該線程就會被阻塞,進入阻塞狀態(tài),當所有其他線程都釋放了這個鎖,并且線程調(diào)度器允許該線程持有這個鎖時,它將變成非阻塞狀態(tài)。
當線程等待另一個線程通知調(diào)度器出現(xiàn)一個條件時,這個線程會進入等待狀態(tài),例如調(diào)用了
Object.wait()、Thread.join()、Lock.tryLock()、Condition.await()等方法如果在調(diào)用上述等待狀態(tài)的方法時傳入了超時參數(shù),線程就回進入計時等待狀態(tài),額外的還有調(diào)用
Thread.sleep()方法也會進入計時等待狀態(tài)。
終止線程
有兩個原因會導致線程終止:
- run方法執(zhí)行完畢,正常退出,線程自然終止
- 發(fā)生了未被捕獲的異常,導致線程意外終止
PS:由于Thread.stop()方法已被廢棄,所以線程無法被手動終止,如需手動終止一個線程,可使用interrupt方法,然后在線程的run方法中不停的檢查當前線程是否被中斷,以決定是否繼續(xù)執(zhí)行run方法中的代碼。
狀態(tài)切換
下圖展示了線程可能的狀態(tài)以及各個狀態(tài)間的切換,當一個線程阻塞或者處于等待狀態(tài)時,線程調(diào)度器可以調(diào)度另一個線程運行,當線程被重新激活,調(diào)度器會檢查他是否比當前運行的線程有更高的優(yōu)先級,如果是,則會剝奪某個線程的運行權(quán),選擇高優(yōu)先級的線程運行。

線程池
在日常開發(fā)中,由于線程的創(chuàng)建和銷毀比較耗費系統(tǒng)資源,所以應(yīng)該盡可能的復(fù)用線程,線程池就提供了線程復(fù)用的能力,在線程池中包含很多準備運行的線程,當我們提交一個runnable對象到線程池中時,就會有線程來調(diào)用它的run方法,當run方法退出時,這個線程不會死亡,而是留在池中等待準備為下一個請求服務(wù)。
可以通過new ThreadPoolExecutor()來構(gòu)造一個自己的線程池,其構(gòu)造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
各個參數(shù)的含義分別是:
- corePoolSize:核心線程數(shù)
- maximumPoolSize:線程池中最大線程數(shù)量
- keepAliveTime:當線程池中空閑線程數(shù)量超過corePoolSize時,多余的線程會在多長時間內(nèi)被銷毀
- unit:keepAliveTime的時間單位
- workQueue:任務(wù)隊列,被添加到線程池中,但尚未被執(zhí)行的任務(wù)
- threadFactory:線程工廠,用于創(chuàng)建線程
- handler:拒絕策略,當任務(wù)太多來不及處理時,如何拒絕任務(wù)
在上面的參數(shù)中,corePoolSize和maximumPoolSize決定了線程池中的線程數(shù)量,其中corePoolSize表示正常情況下的線程數(shù)量,maximumPoolSize表示當任務(wù)隊列已滿時最多創(chuàng)建的線程數(shù)量,keepAliveTime和unit決定了超過核心線程數(shù)的剩余線程的存活時間,threadFactory為線程工廠,一般使用默認的即可,如果需要對線程名等做自定義則需自己提供,workQueue和handler較為復(fù)雜,下面單獨介紹。
workQueue任務(wù)隊列
線程池采用生產(chǎn)者-消費者模型處理處理任務(wù),當生產(chǎn)者通過Executor.execute(Runnable)方法提交一個任務(wù)到線程池時,線程池會根據(jù)當前的任務(wù)執(zhí)行情況和線程的空閑情況決定這個任務(wù)是立即執(zhí)行還是放入一個阻塞隊列中,常用的阻塞隊列有直接提交隊列、有界任務(wù)隊列、無界任務(wù)隊列、優(yōu)先任務(wù)隊列幾種,下面分別說明。
直接提交隊列
使用SynchronousQueue實現(xiàn),包路徑為java.util.concurrent.SynchronousQueue,是一種特殊的阻塞隊列,沒有容量,提交的任務(wù)不會被保存,總是會立即執(zhí)行,如果當前線程池內(nèi)的線程數(shù)量小于maximumPoolSize,則會立即嘗試創(chuàng)建新的線程執(zhí)行任務(wù),否則會直接執(zhí)行拒絕策略。

上圖中核心線程數(shù)為1,最大線程數(shù)為2,拒絕策略為直接拋出異常,通過日志可以發(fā)現(xiàn),在創(chuàng)建并運行了2個線程后,在嘗試提交第3個任務(wù)時,執(zhí)行了拒絕策略,拋出了異常。
有界的任務(wù)隊列
使用ArrayBlockingQueue實現(xiàn),其底層采用數(shù)組實現(xiàn),使用有界任務(wù)隊列當有新的任務(wù)提交時,如果當前線程池中的線程數(shù)量小于corePoolSize,則直接創(chuàng)建新的線程執(zhí)行任務(wù),否則將任務(wù)緩存到隊列中,當隊列已滿時,則繼續(xù)創(chuàng)建線程直到池中的線程數(shù)量達到maximumPoolSize,此時如果還有新的任務(wù)提交,則執(zhí)行拒絕策略。
[圖片上傳失敗...(image-9aeeeb-1614848387267)]
在上圖中,最大線程數(shù)為2,采用有界任務(wù)隊列,可以看到,在執(zhí)行了2個任務(wù)后,直接執(zhí)行了拒絕策略,然后再執(zhí)行了5個任務(wù),總共提交了10個任務(wù),可是最終只執(zhí)行了7個,這是因為開始執(zhí)行了2個任務(wù),然后有界任務(wù)隊列中緩存了5個任務(wù),在提交最后3個任務(wù)時,執(zhí)行了拒絕策略,任務(wù)丟失,未被執(zhí)行。
無界任務(wù)隊列
使用LinkedBlockingQueue實現(xiàn),底層采用鏈表實現(xiàn),可以無限的往隊列中添加任務(wù),線程池中線程的最大的數(shù)量就是corePoolSize,此時的maximumPoolSize參數(shù)是無效的,使用無界任務(wù)隊列需注意任務(wù)的提交與處理之間的平衡,如果大量的任務(wù)堆積無法處理,比較容易造成資源耗盡的情況。

在上圖中,核心線程數(shù)為2,最大線程數(shù)為4,采用無界任務(wù)隊列,共提交了10個任務(wù),通過日志可以看出,只創(chuàng)建了2個線程去執(zhí)行這些任務(wù),并未創(chuàng)建更多的線程去執(zhí)行任務(wù)。
優(yōu)先級任務(wù)隊列
使用PriorityBlockingQueue實現(xiàn),是一個特殊的無界隊列,與無界隊列的區(qū)別在于無界隊列是按照先進先出的順序執(zhí)行任務(wù),而優(yōu)先級隊列是根據(jù)的任務(wù)的優(yōu)先級順序來執(zhí)行任務(wù),任務(wù)的優(yōu)先級通過Comparable接口判斷,如果向采用優(yōu)先級隊列的線程池提交了未實現(xiàn)Comparable接口的任務(wù),則會報ClassCastException。


拒絕策略
在上面的示例中,已經(jīng)使用了ThreadPoolExecutor自帶的AbortPolicy,當任務(wù)無法處理時,執(zhí)行該策略直接拋出了異常,ThreadPoolExecutor還提供了另外幾個拒絕策略可供選擇:
- AbortPolicy:直接拋出異常,阻止系統(tǒng)正常工作
- CallerRunsPolicy:把任務(wù)隊列中的任務(wù)放在調(diào)用者線程當中運行
- DiscardOledestPolicy:丟棄任務(wù)隊列中最老的一個任務(wù),也就是當前任務(wù)隊列中最先被添加進去的,馬上要被執(zhí)行的那個任務(wù),并嘗試再次提交
- DiscardPolicy策略:丟棄無法處理的任務(wù),不予任何處理。當然使用此策略,業(yè)務(wù)場景中需允許任務(wù)的丟失
根據(jù)不同的業(yè)務(wù)場景可以選擇以上幾種策略中的任何一個,另外如果以上策略都無法滿足需求,也可通過實現(xiàn)RejectedExecutionHandler接口,自定義拒絕策略
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
只需實現(xiàn)上面的接口,并在線程池初始化時傳入即可,在需要執(zhí)行拒絕策略時`rejectedExecution就會被調(diào)用,參數(shù)分別為當前提交的任務(wù)和當前的線程池對象。
默認線程池配置
Java通過Executors類提供了很多默認的線程池配置,其內(nèi)部也是通過對上述參數(shù)的不同配置實現(xiàn)的,如果有滿足需求的場景可以直接使用
Executors.newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
采用立即提交隊列,核心線程數(shù)為0,最大線程數(shù)為Integer.MAX_VALUE,線程最長存活60s,適用于有大量短生命周期任務(wù)提交的場景。
Executors.newFixedThreadPool(int)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
核心線程數(shù)和最大線程數(shù)均為傳入的int型參數(shù),采用無界隊列,包含固定數(shù)目的線程數(shù)量,且線程會一直保留。
Executors.newSingleThreadExecutor()
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
只有一個線程的線程池,所有的任務(wù)會被串行提交執(zhí)行。
參考
《Java核心技術(shù)-卷一》