多线程技术研究
多線程技術整理
一、線程基礎
1)并行和并發(fā)
并行:針對多核CPU而言,每個cpu都可以單獨執(zhí)行任務,多個CPU就可以同時執(zhí)行多個任務,是真正意義上的同時運行
并發(fā):針對單核CPU而言,單核CPU根據(jù)某種規(guī)則交替執(zhí)行多個任務,多個任務之間切換的時間很短,看起來像是同時運行,稱為并發(fā)執(zhí)行
現(xiàn)實中系統(tǒng)需要運行的任務很多,因此一般來說,在多核CPU的系統(tǒng)中既存在并行也存在并發(fā),但在單核系統(tǒng)中,只存在并發(fā)
2)任務、進程和線程的區(qū)別
進程(Process):是指運行中的應用程序。每個進程都有自己獨立的內存空間,是操作系統(tǒng)資源分配的基本單位。在java中,每次運行java.exe即創(chuàng)建一個新的虛擬機進程,進程可以看作是線程的一個容器
線程(Thread):是一個進程中單一順序的控制流,是操作系統(tǒng)能夠調度運算的最小單位。線程存在于進程之中,一個進程包含一個或多個線程。當創(chuàng)建一個進程時,會同時創(chuàng)建一個主線程,再由主線程創(chuàng)建子線程。在java中main方法所在的線程就是主線程。
任務(Task):指的是一系列共同達到某一目的的操作,是一個比較抽象的概念,任務可以看作進程也可以看作線程,可以簡單理解為一件事。
3)線程狀態(tài)
\1. 初始(NEW):新創(chuàng)建了一個線程對象,但還沒有調用start()方法。
\2. 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態(tài)籠統(tǒng)的稱為“運行”。
線程對象創(chuàng)建后,其他線程(比如main線程)調用了該對象的start()方法。該狀態(tài)的線程位于可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處于就緒狀態(tài)(ready)。就緒狀態(tài)的線程在獲得CPU時間片后變?yōu)檫\行中狀態(tài)(running)。
\3. 阻塞(BLOCKED):表示線程阻塞于鎖。
\4. 等待(WAITING):進入該狀態(tài)的線程需要等待其他線程做出一些特定動作(通知或中斷)。
\5. 超時等待(TIMED_WAITING):該狀態(tài)不同于WAITING,它可以在指定的時間后自行返回。
\6. 終止(TERMINATED):表示該線程已經執(zhí)行完畢。
4)線程組
線程組(ThreadGroup)簡單來說就是一個線程集合。線程組的出現(xiàn)是為了更方便地管理線程。
線程組是父子結構的,一個線程組可以集成其他線程組,同時也可以擁有其他子線程組。從結構上看,線程組是一個樹形結構,每個線程都隸屬于一個線程組,線程組又有父線程組,這樣追溯下去,可以追溯到一個根線程組——System線程組。
JVM創(chuàng)建的system線程組是用來處理JVM的系統(tǒng)任務的線程組,例如對象的銷毀等。
system線程組的直接子線程組是main線程組,這個線程組至少包含一個main線程,用于執(zhí)行main方法。
main線程組的子線程組就是應用程序創(chuàng)建的線程組。
用戶創(chuàng)建的所有線程都屬于指定線程組,如果沒有顯式指定屬于哪個線程組,那么該線程就屬于默認線程組(即main線程組)。默認情況下,子線程和父線程處于同一個線程組。
此外,只有在創(chuàng)建線程時才能指定其所在的線程組,線程運行中途不能改變它所屬的線程組,也就是說線程一旦指定所在的線程組就不能改變
為什么要使用線程組:
1.安全
同一個線程組的線程是可以相互修改對方的數(shù)據(jù)的。但如果在不同的線程組中,那么就不能“跨線程組”修改數(shù)據(jù),可以從一定程度上保證數(shù)據(jù)安全。
2.批量管理
可以批量管理線程或線程組對象,有效地對線程或線程組對象進行組織或控制。
public static void main(String[] args) {ThreadGroup subThreadGroup1 = new ThreadGroup("subThreadGroup1");ThreadGroup subThreadGroup2 = new ThreadGroup(subThreadGroup1, "subThreadGroup2");System.out.println("subThreadGroup1 parent name = " + subThreadGroup1.getParent().getName());System.out.println("subThreadGroup2 parent name = " + subThreadGroup2.getParent().getName()); } // subThreadGroup1 parent name = main // subThreadGroup2 parent name = subThreadGroup1二、創(chuàng)建線程
java代碼中啟動線程的根本是使用**Thread.start()**方法,實現(xiàn)Runnable接口,或者使用FutureTask之類實現(xiàn)了Runnable接口的類,都需要新建Thread對象,將Runnable接口實例作為參數(shù)傳入;使用線程池時,其源碼也是調用的Thread的start方法。
創(chuàng)建線程涉及到一個核心類和兩個核心接口:
-
Thread:start方法啟動線程,run方法體是需要線程執(zhí)行的任務,啟動后由系統(tǒng)調度線程,當線程占用到cpu時,jvm調用run方法開始執(zhí)行
- public Thread(Runnable target) public Thread(Runnable target, String name)// name指定線程名稱 public Thread(ThreadGroup group, Runnable target, String name,long stackSize) // stackSize 新線程所需的堆棧大小,或零,表示要忽略此參數(shù)。
-
Runnable:為需要交給線程執(zhí)行的任務提供的接口,翻譯為可運行的,因此不需要返回值,只關心運行與否
- void run()
-
Callable:和Runnable類似,但是翻譯為可呼叫的,有呼叫就對應著有應答,因此接口方法有返回值。jdk中存在RunnableAdapter類將Runnable接口適配成Callable接口(返回值為null,Executors.callable),增加創(chuàng)建線程池服務的靈活性
- V call() throws Exception
1)創(chuàng)建線程的方式(TUDO編寫例子)
1)繼承Thread類,重寫run方法,使用子類調用start方法啟動線程,線程調度時會執(zhí)行run方法
2)實現(xiàn)Runnable接口,重寫run方法,將Runnable實例對象傳參給Thread創(chuàng)建Thread對象,使用Thread對象執(zhí)行start方法啟動線程
3)使用線程池提交任務,由線程池管理線程執(zhí)行任務
4)創(chuàng)建FutureTask等實現(xiàn)Runnable接口的類對象,傳入Thread構造函數(shù)作為參數(shù)(面向Runnable接口編程)
2)線程類內方法
TUDO
三、多線程帶來的問題
1)可見性
指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
在多線程環(huán)境下,一個線程對共享變量的操作對其他線程是不可見的。Java提供了volatile來保證可見性,當一個變量被volatile修飾后,表示著線程本地內存無效,當一個線程修改共享變量后他會立即被更新到主內存中,其他線程讀取共享變量時,會直接從主內存中讀取。當然,synchronize和Lock都可以保證可見性。synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中。因此可以保證可見性。
java內存模型(JMM):
JMM決定一個線程對共享變量的寫入何時對另一個線程可見,JMM定義了線程和主內存之間的抽象關系:共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。
volatile保證可見性,不保證原子性:
(1)當寫一個volatile變量時,JMM會把該線程本地內存中的變量強制刷新到主內存中去;
(2)這個寫會操作會導致其他線程中的volatile變量緩存無效。
volatile修飾的變量禁止指令重排:
重排序是指編譯器和處理器為了優(yōu)化程序性能而對指令序列進行排序的一種手段。重排序需要遵守一定規(guī)則:
(1)重排序操作不會對存在數(shù)據(jù)依賴關系的操作進行重排序。
比如:a=1;b=a; 這個指令序列,由于第二個操作依賴于第一個操作,所以在編譯時和處理器運行時這兩個操作不會被重排序。
(2)重排序是為了優(yōu)化性能,但是不管怎么重排序,單線程下程序的執(zhí)行結果不能被改變
比如:a=1;b=2;c=a+b這三個操作,第一步(a=1)和第二步(b=2)由于不存在數(shù)據(jù)依賴關系, 所以可能會發(fā)生重排序,但是c=a+b這個操作是不會被重排序的,因為需要保證最終的結果一定是c=a+b=3。
重排序在單線程下一定能保證結果的正確性,但是在多線程環(huán)境下,可能發(fā)生重排序,影響結果
禁止指令重排即執(zhí)行到volatile變量時,其前面的所有語句都執(zhí)行完,后面所有語句都未執(zhí)行。且前面語句的結果對volatile變量及其后面語句可見。
-
例如懶漢式實現(xiàn)單例模式中雙重檢查,下列1,2,3,4命令是我們希望執(zhí)行的順序,但是如果instance變量沒有使用volatile修飾的時候,經過指令重排可能會變成1,3,2,4,此時達不到單例的效果。使用volatile修飾之后可以保證執(zhí)行順序。
public class Singleton05{private Singleton05(){}private static volatile Singleton05 instance;// 在調用方法的時候再判斷實例是否存在,不存在則新建實例public static Sigleton05 getInstance(){if(instance == null){ // 1synchronized(Singleton05.class){ // 2if(instance == null){ // 3instance = new Singleton04(); // 4}}}return instance;} }
因為volatile修飾的變量不會加鎖,所以其的重量要比synchronized要低,效率要高
2)原子性
定義: 即一個操作或者多個操作 要么全部執(zhí)行并且執(zhí)行的過程不會被任何因素打斷,要么就都不執(zhí)行。
原子性是拒絕多線程操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。簡而言之,在整個操作過程中不會被線程調度器中斷的操作,都可認為是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:
(1)基本類型的讀取和賦值操作,且賦值必須是值賦給變量,變量之間的相互賦值不是原子性操作。
(2)所有引用reference的賦值操作
(3)java.concurrent.Atomic.* 包中所有類的一切操作
可以通過synchronized和Lock來保證原子性。
3)有序性
即程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
Java內存模型中的有序性可以總結為:如果在本線程內觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有操作都是無序的。前半句是指“線程內表現(xiàn)為串行語義”,后半句是指“指令重排序”現(xiàn)象和“工作內存主主內存同步延遲”現(xiàn)象。
在Java內存模型中,為了效率是允許編譯器和處理器對指令進行重排序,當然重排序不會影響單線程的運行結果,但是對多線程會有影響。Java提供volatile來保證一定的有序性。最著名的例子就是單例模式里面的DCL(雙重檢查鎖)。
另外,可以通過synchronized和Lock來保證有序性,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼,相當于是讓線程順序執(zhí)行同步代碼,自然就保證了有序性。
4)死鎖問題
public void add(int m) {synchronized(lockA) { // 獲得lockA的鎖this.value += m;synchronized(lockB) { // 獲得lockB的鎖this.another += m;} // 釋放lockB的鎖} // 釋放lockA的鎖 }public void dec(int m) {synchronized(lockB) { // 獲得lockB的鎖this.another -= m;synchronized(lockA) { // 獲得lockA的鎖this.value -= m;} // 釋放lockA的鎖} // 釋放lockB的鎖 }當兩個線程各自持有不同的鎖,然后各自試圖獲取對方手里的鎖,造成了雙方無限等待下去,這就是死鎖。
死鎖發(fā)生后,沒有任何機制能解除死鎖,只能強制結束JVM進程。
為了避免死鎖,保證獲取鎖的順序一致即可,改寫如下:
public void dec(int m) {synchronized(lockA) { // 獲得lockA的鎖this.value -= m;synchronized(lockB) { // 獲得lockB的鎖this.another -= m;} // 釋放lockB的鎖} // 釋放lockA的鎖 }四、鎖
解決同步問題可以使用synchronized同步代碼塊,同步對象,其是一種可重入鎖,但是synchronized比較重,而且線程獲取鎖時必須一直等待,沒有額外的等待機制,效率較低
1)ReentrantLock:可重入鎖
- 使用lock()和unlock()方法來實現(xiàn)synchronized的功能
- 有其他的方法比如tryLock()設定嘗試獲取鎖,可設定時間,獲取失敗的話可以執(zhí)行其他操作,避免阻塞等待和死鎖
- 使用ReentrantLock需要處理異常,通常在finally中釋放鎖
- 存在抽象靜態(tài)內部類Sync繼承AQS(AbstractQueuedSynchronizer),內部類FairSync和NonfairSync實現(xiàn)Sync
- 創(chuàng)建ReentrantLock時默認是非公平鎖(即多個線程獲取鎖的順序并不是按照申請鎖的順序),synchronized是非公平鎖,ReentrantLock構造函數(shù)傳參傳入true時創(chuàng)建的是公平鎖
ReentrantLock任何時刻,只允許一個線程修改,當線程讀操作比寫操作頻繁的時候效率就不高。此時需要某個鎖允許多個線程同時讀,但只要有一個線程在寫,其他線程就必須等待
2)ReentrantReadWriterLock:可重入讀寫鎖
-
實現(xiàn)ReadWriterLock接口
-
只允許一個線程寫入(其他線程既不能寫入也不能讀取)
-
沒有寫入時,多個線程允許同時讀(提高性能)
-
存在抽象靜態(tài)內部類Sync繼承AQS(AbstractQueuedSynchronizer),內部類FairSync和NonfairSync實現(xiàn)Sync
-
存在靜態(tài)內部類ReadLock和WriterLock,都實現(xiàn)Lock接口,分別實現(xiàn)讀鎖和寫鎖功能
-
同樣存在公平鎖和非公平鎖
-
讀寫操作分別用讀鎖和寫鎖來加鎖,在讀取時,多個線程可以同時獲得讀鎖,這樣就大大提高了并發(fā)讀的執(zhí)行效率。
ReentrantReadWriterLock可以解決多線程同時讀,但只有一個線程能寫的問題。
如果我們深入分析ReentrantReadWriterLock,會發(fā)現(xiàn)它有個潛在的問題:如果有線程正在讀,寫線程需要等待讀線程釋放鎖后才能獲取寫鎖,即讀的過程中不允許寫,這是一種悲觀的讀鎖,有可能造成寫操作遲遲獲取不到鎖(寫?zhàn)囸I)。
StampedLock和ReentrantReadWriterLock相比,改進之處在于:讀的過程中也允許獲取寫鎖后寫入!這樣一來,我們讀的數(shù)據(jù)就可能不一致,所以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。
樂觀鎖的意思就是樂觀地估計讀的過程中大概率不會有寫入,因此被稱為樂觀鎖。反過來,悲觀鎖則是讀的過程中拒絕有寫入,也就是寫入必須等待。顯然樂觀鎖的并發(fā)效率更高,但一旦有小概率的寫入導致讀取的數(shù)據(jù)不一致,需要能檢測出來,再讀一遍就行。
3)StampedLock:蓋章鎖
-
當讀操作數(shù)量和寫操作數(shù)量相差比較大的時候,此鎖的效率較高,然后是Synchronized,再是ReentrantReadWriterLock
-
是不可重入鎖,不能在一個線程中反復獲取同一個鎖
-
和ReadWriteLock相比,寫入的加鎖是完全一樣的,不同的是讀取
-
讀取時可以通過tryOptimisticLock()方法獲得樂觀讀取,返回的是版本號(long stamp),操作完之后通過validate(stamp)驗證版本號是否發(fā)生改變,如果沒有改變,則表示此前沒有寫操作,讀取的數(shù)據(jù)有效,否則表示此前存在寫操作,讀取數(shù)據(jù)無效,需要通過獲取悲觀讀鎖來讀取數(shù)據(jù)
-
寫入的概率不高,程序在絕大部分情況下可以通過樂觀讀鎖獲取數(shù)據(jù),極少數(shù)情況下使用悲觀讀鎖獲取數(shù)據(jù)。
4)無鎖編程
原理:CAS( Compare And Swap比較并替換)算法
CAS有3個操作數(shù),內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
CAS比較與交換的偽代碼可以表示為:
do{
備份舊數(shù)據(jù);
基于舊數(shù)據(jù)構造新數(shù)據(jù);
}while(!CAS( 內存地址,備份的舊數(shù)據(jù),新數(shù)據(jù) ))
java.util.concurrent.atomic包下定義了部分基本類型的原子操作,采用的是CAS算法
-
Atomic類中主要使用的是Unsafe類方法(基本是native方法)
-
適用于計數(shù)器,累加器等
其他見筆記《鎖》
五、Java提供的并發(fā)安全集合類
java.util.concurrent包下
| List | ArrayList | CopyOnWriteArrayList |
| Map | HashMap | ConcurrentHashMap |
| Set | HashSet / TreeSet | CopyOnWriteArraySet |
| Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
| Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
六、線程通信
多線程協(xié)調運行的原則就是:當條件不滿足時,線程進入等待狀態(tài);當條件滿足時,線程被喚醒,繼續(xù)執(zhí)行任務。
1)線程間通信
(1)Objec提供的(native)方法(結合Synchronized使用):由鎖對象調用
- wait():釋放鎖,線程等待,wait方法不會返回。直到鎖對象調用了以下其中一個方法時才會返回,并且需要嘗試重新獲取鎖
- notify():喚醒一個等待此鎖對象的線程,喚醒的線程是隨機的(和操作系統(tǒng)相關),其余沒有喚醒的繼續(xù)等待
- notifyAll():喚醒所有等待此鎖對象的線程,喚醒的線程會嘗試獲得鎖,獲得鎖的線程可以繼續(xù)執(zhí)行,否則繼續(xù)等待。和notify方法一樣,鎖對象調用之后,要執(zhí)行完臨界代碼塊(即同步的區(qū)域)才會釋放鎖
(2)Condition類:結合Lock的實現(xiàn)類使用
- Lock接口中存在返回Condition實例的方法
- await()會釋放當前鎖,進入等待狀態(tài);
- signal()會喚醒某個等待線程;
- signalAll()會喚醒所有等待線程;
- 喚醒線程從await()返回后需要重新獲得鎖。
此外,和tryLock()類似,await()可以在等待指定時間后,如果還沒有被其他線程通過signal()或signalAll()喚醒,可以自己醒來:
if (condition.await(1, TimeUnit.SECOND)) {// 被其他線程喚醒 } else {// 指定時間內沒有被其他線程喚醒 }2)線程內通信
ThreadLocal
線程執(zhí)行的時候,有些變量希望只能在該線程中使用。
在一個線程中,橫跨若干方法調用,需要傳遞的對象,我們通常稱之為上下文(Context),它是一種狀態(tài),可以是用戶身份、任務信息等。
給每個方法增加一個context參數(shù)非常麻煩,而且有些時候,如果調用鏈有無法修改源碼的第三方庫,User對象就傳不進去了。
Java標準庫提供了一個特殊的ThreadLocal,它可以在一個線程中傳遞同一個對象。
-
ThreadLocal是一個類,存在一個靜態(tài)內部類ThreadLocalMap,ThreadLocalMap使用的是Entry<key,value>數(shù)組來存儲,key是threadLocal,value是想要存儲的內容。
-
在Thread類中存在一個變量
ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;// 子類可繼承獲取值引用的直接是ThreadLocal中的靜態(tài)內部類。
在外部使用threadLocal.set(),get(),remove()方法時都會通過Thread.currentThread()獲得當前線程,然后再獲得線程中的threadLocals變量或者創(chuàng)建一個ThreadLocalMap賦值給threadLocals變量,再對該map操作。ThreadLocalMap中Entry的key是該ThreadLocal對象。
-
不需要設置多個值,可以將需要傳遞的內容封裝成一個引用對象(上下文context)進行傳遞,獲取到后再get相應的值便可,
但是一個線程也可以關聯(lián)多個ThreadLocal對象,這也是ThreadLocalMap使用Entry數(shù)組的原因,可以存儲多個threadLocal作為key,計算哈希值,重復的話順延下一個位置。
-
雖然ThreadLocal存儲數(shù)據(jù)是線程獨立的,但是也不能保證線程安全,因為其存儲的數(shù)據(jù)資源有可能是共享的
-
存儲的位置Entry中key是一個弱引用(WeakReference),當key(即threadLocal為null,或者被gc回收后),Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value引用鏈路仍然存在,value的值沒有被回收,當多個線程的value一直在內存堆積時,容易造成內存泄漏。因此在使用threadLocal時,需要在finally里及時調用remove()方法刪除value。
-
值得注意的是:(TUDO 強引用和弱引用)
-
key 使用強引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強引用,如果沒有手動刪除,ThreadLocal不會被回收,導致Entry內存泄漏。
-
key 使用弱引用:引用的ThreadLocal的對象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除,ThreadLocal也會被回收**。value在下一次ThreadLocalMap調用set,get,remove的時候會被清除**。
-
比較兩種情況,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長,如果都沒有手動刪除對應key,都會導致內存泄漏,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內存泄漏,對應的value在下一次ThreadLocalMap調用set,get,remove的時候會被清除。
-
因此,ThreadLocal內存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長,如果沒有手動刪除對應key就會導致內存泄漏,而不是因為弱引用。
-
七、線程池
創(chuàng)建線程需要操作系統(tǒng)資源(線程資源,棧空間等),頻繁創(chuàng)建和銷毀大量線程需要消耗大量時間。
線程池是一種基于池化技術思想來管理線程的工具。在線程池中維護了多個線程,由線程池統(tǒng)一的管理調配線程來執(zhí)行任務。通過線程復用,減少了頻繁創(chuàng)建和銷毀線程的開銷。
1)生命周期
線程池從誕生到死亡,中間會經歷RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED五個生命周期狀態(tài)。
-
RUNNING 表示線程池處于運行狀態(tài),能夠接受新提交的任務且能對已添加的任務進行處理。**RUNNING狀態(tài)是線程池的初始化狀態(tài),線程池一旦被創(chuàng)建就處于RUNNING狀態(tài)。**且其內還沒有線程,當有任務提交時,線程池才會創(chuàng)建新的線程。
-
SHUTDOWN 線程處于關閉狀態(tài),不接受新任務,但可以處理已添加的任務。RUNNING狀態(tài)的線程池調用shutdown后會進入SHUTDOWN狀態(tài)。
-
STOP 線程池處于停止狀態(tài),不接收任務,不處理已添加的任務,且會中斷正在執(zhí)行任務的線程。RUNNING狀態(tài)的線程池調用了shutdownNow后會進入STOP狀態(tài)。
- // 關閉線程池,會阻止新任務提交,但不影響已提交的任務 executor.shutdown(); // 關閉線程池,阻止新任務提交,并且中斷當前正在運行的線程 executor.showdownNow();
-
TIDYING 當所有任務已終止,且任務數(shù)量為0時,線程池會進入TIDYING。當線程池處于SHUTDOWN狀態(tài)時,阻塞隊列中的任務被執(zhí)行完了,且線程池中沒有正在執(zhí)行的任務了,狀態(tài)會由SHUTDOWN變?yōu)門IDYING。當線程處于STOP狀態(tài)時,線程池中沒有正在執(zhí)行的任務時則會由STOP變?yōu)門IDYING。
-
TERMINATED 線程終止狀態(tài)。處于TIDYING狀態(tài)的線程執(zhí)行terminated()后進入TERMINATED狀態(tài)。
根據(jù)上述線程池生命周期狀態(tài)的描述,可以畫出如下所示的線程池生命周期狀態(tài)流程示意圖。
2)創(chuàng)建線程池
主要的核心類
(1)ThreadPoolExecutor
是創(chuàng)建線程池的根本,存在多個不同的構造函數(shù),但最終都會調用一個
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize || // 證明最大線程數(shù) >= 核心線程數(shù)keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler; }-
corePoolSize 表示線程池的核心線程數(shù)。當有任務提交到線程池時,如果線程池中的線程數(shù)小于corePoolSize,那么則直接創(chuàng)建新的線程來執(zhí)行任務。
-
workQueue 任務隊列,它是一個阻塞隊列,用于存儲來不及執(zhí)行的任務的隊列。當有任務提交到線程池的時候,如果線程池中的線程數(shù)大于等于corePoolSize,那么這個任務則會先被放到這個隊列中,等待執(zhí)行。
-
maximumPoolSize 表示線程池支持的最大線程數(shù)量。當一個任務提交到線程池時,線程池中的線程數(shù)大于corePoolSize,并且workQueue已滿,那么則會創(chuàng)建新的線程執(zhí)行任務,但是線程數(shù)要小于等于maximumPoolSize。
-
keepAliveTime 非核心線程空閑時保持存活的時間。非核心線程即workQueue滿了之后,再提交任務時創(chuàng)建的線程,因為這些線程不是核心線程,所以它空閑時間超過keepAliveTime后則會被回收。
-
unit 非核心線程空閑時保持存活的時間的單位
-
創(chuàng)建線程池時構造函數(shù)中的keepAliveTime和 unit 控制非核心線程的存活時間,即非核心線程一段時間后會被銷毀;
allowCoreThreadTimeOut設置為true時,keepAliveTime和 unit設置的時間對核心線程同樣有效,即默認情況下,核心線程在線程池關閉的情況下會一直存活,如此一來避免了頻繁創(chuàng)建和銷毀線程所帶來的消耗。
-
threadFactory 創(chuàng)建線程的工廠,可以在這里統(tǒng)一處理創(chuàng)建線程的屬性
-
handler 拒絕策略,當線程池中的線程達到maximumPoolSize線程數(shù)后且workQueue已滿的情況下,再向線程池提交任務則執(zhí)行對應的拒絕策略
(2)Executors
封裝了一些快速簡便創(chuàng)建線程池的方法,構造函數(shù)內調用的也是ThreadPoolExecutor或者其子類的構造函數(shù)
// 例如: // 實例化一個單線程的線程池 ExecutorService singleExecutor = Executors.newSingleThreadExecutor(); // 創(chuàng)建固定線程個數(shù)的線程池 ExecutorService fixedExecutor = Executors.newFixedThreadPool(10); // 創(chuàng)建一個可重用固定線程數(shù)的線程池 ExecutorService executorService2 = Executors.newCachedThreadPool();(3)ExecutorService
繼承Executor接口
// Executor void execute(Runnable command); // 提交Runnable任務,沒有返回值// ExecutorService <T> Future<T> submit(Callable<T> task); // 提價Callable任務,并且返回實現(xiàn)Callable接口時傳參的類型 <T> Future<T> submit(Runnable task, T result); // 提交Runnable任務,自定義返回類型 Future<?> submit(Runnable task); //3)接收結果
-
Future:一個Future類型的實例代表一個未來能獲取結果的對象
- 在主線程某個時刻調用Future對象的get()方法,就可以獲得異步執(zhí)行的結果。在調用get()時,如果異步任務已經完成,我們就直接獲得結果。如果異步任務還沒有完成,那么get()會阻塞,直到任務完成后才返回結果。
- get(long timeout, TimeUnit unit):獲取結果,但只等待指定的時間;
- cancel(boolean mayInterruptIfRunning):取消當前任務;
- isDone():判斷任務是否已完成。
-
CompletableFuture :因為使用Future的方法時有可能會阻塞線程,CompletableFuture中提供了許多方法可以使用lambda方式傳入回調執(zhí)行對象,可以選擇不同的情況下需要執(zhí)行的方法,使得任務執(zhí)行更加靈活。
- 詳見筆記《CompletableFuture》
4)線程池工作流程
線程池提交任務是從execute/submit方法開始的,我們可以從execute方法來分析線程池的工作流程。
(1)當execute方法提交一個任務時,如果線程池中線程數(shù)小于corePoolSize,那么不管線程池中是否有空閑的線程,都會創(chuàng)建一個新的線程來執(zhí)行任務。
(2)當execute方法提交一個任務時,線程池中的線程數(shù)已經達到了corePoolSize,且此時沒有空閑的線程,那么則會將任務存儲到workQueue中。
(3)如果execute提交任務時線程池中的線程數(shù)已經到達了corePoolSize,并且workQueue已滿,那么則會創(chuàng)建新的線程來執(zhí)行任務,但總線程數(shù)應該小于maximumPoolSize。
(4)如果線程池中的線程執(zhí)行完了當前的任務,則會嘗試從workQueue中取出第一個任務來執(zhí)行。如果workQueue為空則會阻塞線程。
(5)如果execute提交任務時,線程池中的線程數(shù)達到了maximumPoolSize,且workQueue已滿,此時會執(zhí)行拒絕策略來拒絕接受任務。
(6)如果線程池中的線程數(shù)超過了corePoolSize,那么空閑時間超過keepAliveTime的線程會被銷毀,但程池中線程個數(shù)會保持為corePoolSize。
(7)如果線程池存在空閑的線程,并且設置了allowCoreThreadTimeOut為true。那么空閑時間超過keepAliveTime的線程都會被銷毀。
5)線程池的拒絕策略
如果線程池中的線程數(shù)達到了maximumPoolSize,并且workQueue隊列存儲滿的情況下,線程池會執(zhí)行對應的拒絕策略。在JDK中提供了RejectedExecutionHandler接口來執(zhí)行拒絕操作。實現(xiàn)RejectedExecutionHandler的類有四個,對應了四種拒絕策略。分別如下:
-
DiscardPolicy 當提交任務到線程池中被拒絕時,線程池會丟棄這個被拒絕的任務
-
DiscardOldestPolicy 當提交任務到線程池中被拒絕時,線程池會丟棄等待隊列中最老的任務。
-
CallerRunsPolicy 當提交任務到線程池中被拒絕時,會在線程池當前正在運行的Thread線程中處理被拒絕的任務。即哪個線程提交的任務哪個線程去執(zhí)行。
-
AbortPolicy 當提交任務到線程池中被拒絕時,直接拋出RejectedExecutionException異常。
八、多線程的使用場景
- 系統(tǒng)吞吐量高
- 多并發(fā)
- 后臺任務
- 異步處理
- 分布式計算
九、參考內容
廖雪峰的官方網站 (liaoxuefeng.com)
源碼系列 之 ThreadLocal_小夏陌的博客-CSDN博客
重要!!!徹底搞懂Java線程池的工作原理-51CTO.COM
Java面試必問,ThreadLocal終極篇 - 簡書 (jianshu.com)
Java并發(fā) 之 線程組 ThreadGroup 介紹 - 知乎 (zhihu.com)
并發(fā)編程系列之什么是ForkJoin框架? - 簡書 (jianshu.com)
任務、進程、線程之間的區(qū)別_阿文的博客-CSDN博客_任務線程
面試題:聊聊線程和進程的區(qū)別(精心梳理)_黑桃A的博客-CSDN博客
重要!!!JDK ThreadPoolExecutor核心原理與實踐 - 簡書 (jianshu.com)
一文秒懂 Java ExecutorService - Java 一文秒懂 - 簡單教程,簡單編程 (twle.cn)
java 進程和線程的區(qū)別與聯(lián)系_hp_yangpeng的博客-CSDN博客_java 進程和線程的區(qū)別
進程、線程、服務和任務的區(qū)別以及多線程與超線程的概念 - Lxk- - 博客園 (cnblogs.com)
Java并發(fā)(8)- 讀寫鎖中的性能之王:StampedLock - knock_小新 - 博客園 (cnblogs.com)
弱引用什么時候被回收_面試官:ThreadLocal為什么會發(fā)生內存泄漏?_weixin_39948210的博客-CSDN博客
Java volatile關鍵字最全總結:原理剖析與實例講解(簡單易懂)_老鼠只愛大米的博客-CSDN博客_java volatile
參考鏈接可能不全,侵權刪。
總結
- 上一篇: 抖音开屏广告和信息流广告相比较哪一种效果
- 下一篇: 前端简单防抖功能