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