突擊并發編程JUC系列演示代碼地址:
https://github.com/mtcarpenter/JavaTutorial
什么是 CAS 嗎?
CAS(Compare And Swap)
指比較并交換。CAS
算法CAS(V, E, N)
包含 3 個參數,V 表示要更新的變量,E 表示預期的值,N 表示新值。在且僅在 V 值等于 E值時,才會將 V 值設為 N,如果 V 值和 E 值不同,則說明已經有其他線程做了更新,當前線程什么都不做。最后,CAS 返回當前 V 的真實值。Concurrent
包下所有類底層都是依靠CAS
操作來實現,而sun.misc.Unsafe
為我們提供了一系列的CAS
操作。
CAS 有什么缺點?
-
ABA
問題 - 自旋問題
- 范圍不能靈活控制
對 CAS 中的 ABA 產生有解決方案嗎?
什么是 ABA 問題呢?多線程環境下。線程 1 從內存的V位置取出 A ,線程 2 也從內存中取出 A,并將 V 位置的數據首先修改為 B,接著又將 V 位置的數據修改為 A,線程 1 在進行CAS
操作時會發現在內存中仍然是 A,線程 1 操作成功。盡管從線程 1 的角度來說,CAS
操作是成功的,但在該過程中其實 V 位置的數據發生了變化,線程 1 沒有感知到罷了,這在某些應用場景下可能出現過程數據不一致的問題。
可以版本號(version)來解決 ABA 問題的,在 atomic
包中提供了AtomicStampedReference
這個類,它是專門用來解決 ABA 問題的。
CAS 自旋導致的問題?
由于單次 CAS
不一定能執行成功,所以 CAS
往往是配合著循環來實現的,有的時候甚至是死循環,不停地進行重試,直到線程競爭不激烈的時候,才能修改成功。
CPU 資源也是一直在被消耗的,這會對性能產生很大的影響。所以這就要求我們,要根據實際情況來選擇是否使用 CAS
,在高并發的場景下,通常 CAS
的效率是不高的。
CAS 范圍不能靈活控制
不能靈活控制線程安全的范圍。只能針對某一個,而不是多個共享變量的,不能針對多個共享變量同時進行 CAS
操作,因為這多個變量之間是獨立的,簡單的把原子操作組合到一起,并不具備原子性。
什么是 AQS 嗎?
AbstractQueuedSynchronizer
抽象同步隊列簡稱AQS
,它是實現同步器的基礎組件,并發包中鎖的底層就是使用AQS
實現的。AQS
定義了一套多線程訪問共享資源的同步框架,許多同步類的實現都依賴于它,例如常用的Synchronized
、ReentrantLock
、ReentrantReadWriteLock
、Semaphore
、CountDownLatch
等。該框架下的鎖會先嘗試以CAS
樂觀鎖去獲取鎖,如果獲取不到,則會轉為悲觀鎖(如RetreenLock
)。
了解 AQS 共享資源的方式嗎?
- 獨占式:只有一個線程能執行,具體的Java實現有
ReentrantLock
。 - 共享式:多個線程可同時執行,具體的Java實現有
Semaphore
和CountDownLatch
。
Atomic 原子更新
Java
從 JDK1.5
開始提供了 java.util.concurrent.atomic
包,方便程序員在多線程環 境下,無鎖的進行原子操作。在 Atomic
包里一共有 12 個類,四種原子更新方式,分別是原子更新基本類型
,原子更新數組
,原子更新引用
和原子更新字段
。在 JDK 1.8
之后又新增幾個原子類。如下如:
針對思維導圖知識點在前面的章節都進行了理論+實踐的講解,到達地址如下:
突擊并發編程JUC系列-原子更新AtomicLong
突擊并發編程JUC系列-數組類型AtomicLongArray
突擊并發編程JUC系列-原子更新字段類AtomicStampedReference
突擊并發編程JUC系列-JDK1.8 擴展類型 LongAdder
列舉幾個AtomicLong
的常用方法
-
long getAndIncrement()
:以原子方式將當前值加1,注意,返回的是舊值。(i++) -
long incrementAndGet()
:以原子方式將當前值加1,注意,返回的是新值。(++i) -
long getAndDecrement()
:以原子方式將當前值減 1,注意,返回的是舊值 。(i--) -
long decrementAndGet()
:以原子方式將當前值減 1,注意,返回的是新值 。(--i) -
long addAndGet(int delta)
:以原子方式將輸入的數值與實例中的值(AtomicLong
里的value
)相加,并返回結果
說說 AtomicInteger 和 synchronized 的異同點?
相同點
- 都是線程安全
不同點
- 1、背后原理
synchronized 背后的 monitor 鎖。在執行同步代碼之前,需要首先獲取到 monitor 鎖,執行完畢后,再釋放鎖。原子類,線程安全的原理是利用了 CAS 操作。 - 2、使用范圍
原子類使用范圍是比較局限的,一個原子類僅僅是一個對象,不夠靈活。而synchronized
的使用范圍要廣泛得多。比如說 synchronized 既可以修飾一個方法,又可以修飾一段代碼,相當于可以根據我們的需要,非常靈活地去控制它的應用范圍 - 3、粒度
原子變量的粒度是比較小的,它可以把競爭范圍縮小到變量級別。通常情況下,synchronized
鎖的粒度都要大于原子變量的粒度。 - 4、性能
synchronized
是一種典型的悲觀鎖,而原子類恰恰相反,它利用的是樂觀鎖。
原子類和 volatile 有什么異同?
-
volatile
可見性問題 - 解決原子性問題
AtomicLong 可否被 LongAdder 替代?
有了更高效的 LongAdder
,那AtomicLong
可否不使用了呢?是否凡是用到 AtomicLong
的地方,都可以用LongAdder
替換掉呢?答案是不是的,這需要區分場景。
LongAdder
只提供了 add
、increment
等簡單的方法,適合的是統計求和計數的場景,場景比較單一,而 AtomicLong
還具有 compareAndSet
等高級方法,可以應對除了加減之外的更復雜的需要CAS
的場景。
結論:如果我們的場景僅僅是需要用到加和減操作的話,那么可以直接使用更高效的 LongAdder
,但如果我們需要利用 CAS
比如compareAndSet
等操作的話,就需要使用 AtomicLong
來完成。
并發工具
CountDownLatch
CountDownLatch
基于線程計數器來實現并發訪問控制,主要用于主線程等待其他子線程都執行完畢后執行相關操作。其使用過程為:在主線程中定義CountDownLatch
,并將線程計數器的初始值設置為子線程的個數,多個子線程并發執行,每個子線程在執行完畢后都會調用countDown
函數將計數器的值減1,直到線程計數器為0,表示所有的子線程任務都已執行完畢,此時在CountDownLatch
上等待的主線程將被喚醒并繼續執行。
CyclicBarrier
CyclicBarrier
(循環屏障)是一個同步工具,可以實現讓一組線程等待至某個狀態之后再全部同時執行。在所有等待線程都被釋放之后,CyclicBarrier
可以被重用。CyclicBarrier
的運行狀態叫作Barrier
狀態,在調用await
方法后,線程就處于Barrier
狀態。
CyclicBarrier
中最重要的方法是await方法,它有兩種實現。
-
public int await()
:掛起當前線程直到所有線程都為Barrier狀態再同時執行后續的任務。 -
public int await(long timeout, TimeUnit unit)
:設置一個超時時間,在超時時間過后,如果還有線程未達到Barrier
狀態,則不再等待,讓達到Barrier狀態的線程繼續執行后續的任務。
Semaphore
Semaphore
指信號量,用于控制同時訪問某些資源的線程個數,具體做法為通過調用acquire()
獲取一個許可,如果沒有許可,則等待,在許可使用完畢后通過release()
釋放該許可,以便其他線程使用。
CyclicBarrier 和 CountdownLatch 有什么異同?
相同點:都能阻塞一個或一組線程,直到某個預設的條件達成發生,再統一出發。
但是它們也有很多不同點,具體如下。
- 作用對象不同:
CyclicBarrier
要等固定數量的線程都到達了柵欄位置才能繼續執行,而CountDownLatch
只需等待數字倒數到 0,也就是說CountDownLatch
作用于事件,但CyclicBarrier
作用于線程;CountDownLatch
是在調用了countDown
方法之后把數字倒數減 1,而CyclicBarrier
是在某線程開始等待后把計數減 1。 - 可重用性不同:
CountDownLatch
在倒數到 0 并且觸發門閂打開后,就不能再次使用了,除非新建一個新的實例;而CyclicBarrier
可以重復使用。CyclicBarrier
還可以隨時調用 reset 方法進行重置,如果重置時有線程已經調用了 await 方法并開始等待,那么這些線程則會拋出BrokenBarrierException
異常。 - 執行動作不同:
CyclicBarrier
有執行動作barrierAction
,而CountDownLatch
沒這個功能。
CountDownLatch、CyclicBarrier、Semaphore的區別如下。
-
CountDownLatch
和CyclicBarrier
都用于實現多線程之間的相互等待,但二者的關注點不同。CountDownLatch
主要用于主線程等待其他子線程任務均執行完畢后再執行接下來的業務邏輯單元,而CyclicBarrier
主要用于一組線程互相等待大家都達到某個狀態后,再同時執行接下來的業務邏輯單元。此外,CountDownLatch
是不可以重用的,而CyclicBarrier
是可以重用的。 -
Semaphore
和Java
中的鎖功能類似,主要用于控制資源的并發訪問。
locks
公平鎖與非公平鎖
ReentrantLock
支持公平鎖和非公平鎖兩種方式。公平鎖指鎖的分配和競爭機制是公平的,即遵循先到先得原則。非公平鎖指JVM
遵循隨機、就近原則分配鎖的機制。ReentrantLock
通過在構造函數ReentrantLock(boolean fair)
中傳遞不同的參數來定義不同類型的鎖,默認的實現是非公平鎖。這是因為,非公平鎖雖然放棄了鎖的公平性,但是執行效率明顯高于公平鎖。如果系統沒有特殊的要求,一般情況下建議使用非公平鎖。
synchronized 和 lock 有什么區別?
-
synchronized
可以給類,方法,代碼塊加鎖,而lock
只能給代碼塊加鎖。 -
synchronized
不需要手動獲取鎖和釋放鎖,使用簡單,發生異常會自動釋放鎖,不會造成死鎖,而lock
需要手動自己加鎖和釋放鎖,如果使用不當沒有unLock
去釋放鎖,就會造成死鎖。 - 通過
lock
可以知道有沒有成功獲取鎖,而synchronized
無法辦到。
synchronized 和 Lock 如何選擇?
-
synchronized
和Lock
都是用來保護資源線程安全的。 - 都保證了可見性和互斥性。
-
synchronized
和ReentrantLock
都擁有可重入的特點。
不同點:
- 用法(
lock
需要配合finally
) -
ReentrantLock
可響應中斷、可輪回,為處理鎖提供了更多的靈活性 -
ReentrantLock
通過Condition
可以綁定多個條件 - 加解鎖順序()
-
synchronized
鎖不夠靈活 - 是否可以設置公平/非公平
- 二者的底層實現不一樣:
synchronized
是同步阻塞,采用的是悲觀并發策略;Lock
是同步非阻塞,采用的是樂觀并發策略。
使用
- 如果能不用最好既不使用
Lock
也不使用synchronized
。 - 如果
synchronized
關鍵字適合你的程序,這樣可以減少編寫代碼的數量,減少出錯的概率 - 如果特別需要
Lock
的特殊功能,比如嘗試獲取鎖、可中斷、超時功能等,才使用Lock
。
Lock接口的主要方法
-
void lock()
:獲取鎖,調用該方法當前線程將會獲取鎖,當鎖獲得后,從該方法返回 -
void lockInterruptibly() throws InterruptedException
:可中斷地獲取鎖,和lock
方法地不同之處在于該方法會響應中斷,即在鎖的獲取中可以中斷當前線程 -
boolean tryLock()
: 嘗試非阻塞地獲取鎖,調用該方法后立刻返回,如果能夠獲取則返回 true 否則 返回false -
boolean tryLock(long time, TimeUnit unit)
:超時地獲取鎖,當前線程在以下 3 種情況下會返回: - 當前線程在超時時間內獲得了鎖
- 當前線程在超時時間被中斷
- 超時時間結束后,返回 false
-
void unlock()
: 釋放鎖 -
Condition newCondition()
:獲取鎖等待通知組件,該組件和當前的鎖綁定,當前線程只有獲得了鎖,才能調用該組件的wait()
方法,而調用后,當前線程將釋放鎖。
tryLock、lock和lockInterruptibly的區別
tryLock
、lock
和lockInterruptibly
的區別如下。
-
tryLock
若有可用鎖,則獲取該鎖并返回true,否則返回false,不會有延遲或等待;tryLock(long timeout, TimeUnit unit)
可以增加時間限制,如果超過了指定的時間還沒獲得鎖,則返回 false。 -
lock
若有可用鎖,則獲取該鎖并返回true,否則會一直等待直到獲取可用鎖。 - 在鎖中斷時
lockInterruptibly
會拋出異常,lock
不會。
ReentrantReadWriteLock 讀寫鎖的獲取規則
要么是一個或多個線程同時有讀鎖,要么是一個線程有寫鎖,但是兩者不會同時出現。也可以總結為:讀讀共享、其他都互斥(寫寫互斥、讀寫互斥、寫讀互斥)
ReentrantLock
適用于一般場合,ReadWriteLock
適用于讀多寫少的情況,合理使用可以進一步提高并發效率。
讀鎖應該插隊嗎?什么是讀寫鎖的升降級?
ReentrantReadWriteLock
的實現選擇了“不允許插隊”的策略,這就大大減小了發生“饑餓”的概率。
插隊策略
- 公平策略下,只要隊列里有線程已經在排隊,就不允許插隊。
- 非公平策略下:
- 如果允許讀鎖插隊,那么由于讀鎖可以同時被多個線程持有,所以可能造成源源不斷的后面的線程一直插隊成功,導致讀鎖一直不能完全釋放,從而導致寫鎖一直等待,為了防止“饑餓”,在等待隊列的頭結點是嘗試獲取寫鎖的線程的時候,不允許讀鎖插隊。
- 寫鎖可以隨時插隊,因為寫鎖并不容易插隊成功,寫鎖只有在當前沒有任何其他線程持有讀鎖和寫鎖的時候,才能插隊成功,同時寫鎖一旦插隊失敗就會進入等待隊列,所以很難造成“饑餓”的情況,允許寫鎖插隊是為了提高效率。
升降級策略:只能從寫鎖降級為讀鎖,不能從讀鎖升級為寫鎖。
怎么防止死鎖?
- 盡量使用
tryLock(long timeout,TimeUnit unit)
的方法(ReentrantLock 、ReenttranReadWriteLock
)設置超時時間,超時可以退出防止死鎖。 - 盡量使用
java.util.concurrent
并發類代替手寫鎖。 - 盡量降低鎖的使用粒度,盡量不要幾個功能用同一把鎖。
- 盡量減少同步的代碼塊。
Condition 類和 Object 類鎖方法區別區別
-
Condition
類的awiat
方法和Object
類的wait
方法等效 -
Condition
類的signal
方法和Object
類的notify
方法等效 -
Condition
類的signalAll
方法和Object
類的notifyAll
方法等效 -
ReentrantLock
類可以喚醒指定條件的線程,而 object 的喚醒是隨機的
并發容器
為什么 ConcurrentHashMap 比 HashTable 效率要高?
-
HashTable
使用一把鎖(鎖住整個鏈表結構)處理并發問題,多個線程競爭一把鎖,容易阻塞; -
ConcurrentHashMap
-
JDK 1.7
中使用分段鎖(ReentrantLock + Segment + HashEntry
),相當于把一個HashMap
分成多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基于 Segment,包含多個HashEntry
。 -
JDK 1.8
中使用CAS + synchronized + Node + 紅黑樹
。鎖粒度:Node(首結點)(實現Map.Entry
)。鎖粒度降低了。
-
ConcurrentHashMap JDK 1.7/JDK 1.8
JDK 1.7 結構
JDK 1.7
中的ConcurrentHashMap
內部進行了 Segment
分段,Segment
繼承了 ReentrantLock
,可以理解為一把鎖,各個 Segment
之間都是相互獨立上鎖的,互不影響。
相比于之前的 Hashtable
每次操作都需要把整個對象鎖住而言,大大提高了并發效率。因為它的鎖與鎖之間是獨立的,而不是整個對象只有一把鎖。
每個 Segment 的底層數據結構與 HashMap
類似,仍然是數組和鏈表組成的拉鏈法結構。默認有 0~15 共 16 個 Segment
,所以最多可以同時支持 16 個線程并發操作(操作分別分布在不同的 Segment
上)。16 這個默認值可以在初始化的時候設置為其他值,但是一旦確認初始化以后,是不可以擴容的。
JDK 1.8 結構
圖中的節點有三種類型:
- 第一種是最簡單的,空著的位置代表當前還沒有元素來填充。
- 第二種就是和
HashMap
非常類似的拉鏈法結構,在每一個槽中會首先填入第一個節點,但是后續如果計算出相同的 Hash 值,就用鏈表的形式往后進行延伸。 - 第三種結構就是紅黑樹結構,這是 Java 7 的
ConcurrentHashMap
中所沒有的結構,在此之前我們可能也很少接觸這樣的數據結構
鏈表長度大于某一個閾值(默認為 8),滿足容量從鏈表的形式轉化為紅黑樹的形式。
紅黑樹是每個節點都帶有顏色屬性的二叉查找樹,顏色為紅色或黑色,紅黑樹的本質是對二叉查找樹 BST 的一種平衡策略,我們可以理解為是一種平衡二叉查找樹,查找效率高,會自動平衡,防止極端不平衡從而影響查找效率的情況發生,紅黑樹每個節點要么是紅色,要么是黑色,但根節點永遠是黑色的。
ConcurrentHashMap 中 get 的過程
- 計算 Hash 值,并由此值找到對應的槽點;
- 如果數組是空的或者該位置為 null,那么直接返回 null 就可以了;
- 如果該位置處的節點剛好就是我們需要的,直接返回該節點的值;
- 如果該位置節點是紅黑樹或者正在擴容,就用 find 方法繼續查找;
- 否則那就是鏈表,就進行遍歷鏈表查找
ConcurrentHashMap 中 put 的過程
- 判斷
Node[]
數組是否初始化,沒有則進行初始化操作 - 通過
hash
定位數組的索引坐標,是否有Node
節點,如果沒有則使用CAS
進行添加(鏈表的頭節點),添加失敗則進入下次循環。 - 檢查到內部正在擴容,就幫助它一塊擴容。
- 如果 f != null ,則使用
synchronized
鎖住 f 元素(鏈表/紅黑二叉樹的頭元素) - 如果是
Node
(鏈表結構)則執行鏈表的添加操作 - 如果是
TreeNode
(樹形結構)則執行樹添加操作。
- 如果是
- 判斷鏈表長度已經達到臨界值 8 ,當然這個 8 是默認值,大家也可以去做調整,當節點數超過這個值就需要把鏈表轉換為樹結構。
什么是阻塞隊列?
阻塞隊列(BlockingQueue
)是一個支持兩個附加操作的隊列。這兩個附加的操作支持阻塞的插入和移除方法。
- 支持阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的線程,直到隊列不滿。
- 支持阻塞的移除方法:意思是在隊列為空時,獲取元素的線程會等待隊列變為非空。
阻塞隊列常用于生產者和消費者的場景,生產者是向隊列里添加元素的線程,消費者是從隊列里取元素的線程。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器。
列舉幾個常見的阻塞隊列
-
ArrayBlockingQueue
:一個由數組結構組成的有界阻塞隊列。 -
LinkedBlockingQueue
:一個由鏈表結構組成的有界阻塞隊列。 -
PriorityBlockingQueue
:一個支持優先級排序的無界阻塞隊列。 -
DelayQueue
:一個使用優先級隊列實現的無界阻塞隊列。 -
SynchronousQueue
:一個不存儲元素的阻塞隊列。 -
LinkedTransferQueue
:一個由鏈表結構組成的無界阻塞隊列。 -
LinkedBlockingDeque
:一個由鏈表結構組成的雙向阻塞隊列。
線程池
使用線程池的優勢
Java 中的線程池是運用場景最多的并發框架,幾乎所有需要異步或并發執行任務的程序都可以使用線程池。
- 降低資源消耗。 通過重復利用已創建的線程降低線程創建和銷毀造成的消耗。
- 提高響應速度。 當任務到達時,任務可以不需要等到線程創建就能立即執行。
- 提高線程的可管理性。 線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用線程池,必須對其實現原理了如指掌。
線程池的實現原理
當提交一個新任務到線程池時,線程池的處理流程如下:
- 線程池判斷核心線程池里的線程是否都在執行任務。如果不是,則創建一個新的工作線程來執行任務。如果核心線程池里的線程都在執行任務,則進入下個流程。
- 線程池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程。
-
線程池判斷線程池的線程是否都處于工作狀態。如果沒有,則創建一個新的工作線程來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
ThreadPoolExecutor
執行execute()
方法的示意圖 如下:
ThreadPoolExecutor
執行execute
方法分下面 4 種情況:
- 1、如果當前運行的線程少于
corePoolSize
,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。 - 2、如果運行的線程等于或多于
corePoolSize
,則將任務加入BlockingQueue
。 - 3、如果無法將任務加入
BlockingQueue
(隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。 - 4、如果創建新線程將使當前運行的線程超出
maximumPoolSize
,任務將被拒絕,并調用RejectedExecutionHandler.rejectedExecution()
方法。
ThreadPoolExecutor
采取上述步驟的總體設計思路,是為了在執行execute()方法時,盡可能地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor
完成預熱之后(當前運行的線程數大于等于corePoolSize
),幾乎所有的execute()
方法調用都是執行步驟 2,而步驟2不需要獲取全局鎖。
創建線程有三種方式:
- 繼承 Thread 重寫 run 方法
- 實現 Runnable 接口
- 實現 Callable 接口 (有返回值)
線程有哪些狀態?
-
NEW(初始)
,新建狀態,線程被創建出來,但尚未啟動時的線程狀態; -
RUNNABLE(就緒狀態)
,表示可以運行的線程狀態,它可能正在運行,或者是在排隊等待操作系統給它分配 CPU 資源; -
BLOCKED(阻塞)
,阻塞等待鎖的線程狀態,表示處于阻塞狀態的線程正在等待監視器鎖,比如等待執行synchronized
代碼塊或者使用synchronized
標記的方法; -
WAITING(等待)
,等待狀態,一個處于等待狀態的線程正在等待另一個線程執行某個特定的動作,比如,一個線程調用了Object.wait()
方法,那它就在等待另一個線程調用Object.notify()
或Object.notifyAll()
方法; -
TIMED_WAITING(超時等待)
,計時等待狀態,和等待狀態(WAITING)
類似,它只是多了超時時間,比如調用了有超時時間設置的方法Object.wait(long timeout)
和Thread.join(long timeout)
等這些方法時,它才會進入此狀態; -
TERMINATED
,終止狀態,表示線程已經執行完成。
線程池的狀態有那些?
-
running
:這是最正常的狀態,接受新的任務,處理等待隊列中的任務。 -
shutdown
:不接受新的任務提交,但是會繼續處理等待隊列中的任務。 -
stop
:不接受新的任務提交,不再處理等待隊列中的任務,中斷正在執行任務的線程。 -
tidying
:所有的任務都銷毀了,workcount
為 0,線程池的狀態再轉換 tidying 狀態時,會執行鉤子方法terminated()
。 -
terminated
:terminated()
方法結束后,線程池的狀態就會變成這個。
線程池中 sumbit() 和 execute() 方法有什么區別?
-
execute()
: 只能執行Runable
類型的任務。 -
submit()
可以執行Runable
和Callable
類型的任務。
? Callable
類型的任務可以獲取執行的返回值,而 Runnable
執行無返回值。
線程池創建的方式
-
newSingleThreadExecutor()
: 他的特點是在于線程數目被限制位1:操作一個無界的工作隊列,所以它保證了所有的任務的都是順序執行,最多會有一個任務處于活動狀態,并且不允許使用者改動線程池實例,因此可以避免其改變線程數目。 -
newCachedThreadPool()
:它是一種用來處理大量短時間工作任務的線程,具有幾個鮮明的特點,它會試圖緩存線程并重用,當無緩存線程可用時,就會創建新的工作線程,如果線程閑置的時間超過 60 秒,則被終止并移除緩存;長時間閑置時,這種線程池不會消耗什么資源,其內部使用synchronousQueue
作為工作隊列。 -
newFixedThreadPool(int nThreads)
:重用指定數目 nThreads 的線程,其背后使用的無界的工作隊列,任何時候最后有 nThreads 個工作線程活動的,這意味著 如果任務數量超過了活動隊列數目,將在工作隊列中等待空閑線程出現,如果有工作線程退出,將會有新的工作線程被創建,以補足指定的數目 nThreads。 -
newSingleThreadScheduledExecutor()
: 創建單線程池,返回ScheduleExecutorService
可以進行定時或周期性的工作強度。 -
newScheduleThreadPool(int corePoolSize)
: 和newSingleThreadSceduleExecutor()
類似,創建的ScheduledExecutorService
可以進行定時或周期的工作調度,區別在于單一工作線程還是工作線程。 -
newWorkStrealingPool(int parallelism)
:這是一個經常被人忽略的線程池,Java 8 才加入這個創建方法,其內部會構建ForkJoinPool
利用work-strealing
算法 并行的處理任務,不保證處理順序。 -
ThreadPollExecutor
: 是最原始的線程池創建,上面 1-3 創建方式 都是對ThreadPoolExecutor
的封裝。
上面 7 種創建方式中,前 6 種 通過Executors
工廠方法創建,ThreadPoolExecutor
手動創建。
ThreadPollExecutor 構造方法
下面介紹下 ThreadPoolExecutor
接收 7 個參數的構造方法
/**
* 用給定的初始參數創建一個新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量
int maximumPoolSize,//線程池的最大線程數
long keepAliveTime,//當線程數大于核心線程數時,多余的空閑線程存活的最長時間
TimeUnit unit,//時間單位
BlockingQueue<Runnable> workQueue,//任務隊列
ThreadFactory threadFactory,//線程工廠
RejectedExecutionHandler handler//拒絕策略
)
-
corePoolSize
: 核心線程數線程數定義了最小可以同時運行的線程數量。 -
maximumPoolSize
: 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數量變為最大線程數。 -
workQueue
: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,如果達到的話,信任就會被存放在隊列中。 -
keepAliveTime
:線程活動保持時間,當線程池中的線程數量大于corePoolSize
的時候,如果這時沒有新的任務提交,核心線程外的線程不會立即銷毀,而是會等待,直到等待的時間超過了keepAliveTime
才會被回收銷毀; -
unit
:keepAliveTime
參數的時間單位。 -
threadFactory
: 任務隊列,用于保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列。-
ArrayBlockingQueue
:是一個基于數組結構的有界阻塞隊列,此隊列按FIFO
(先進先出)原則對元素進行排序。 -
LinkedBlockingQueue
:一個基于鏈表結構的阻塞隊列,此隊列按FIFO
排序元素,吞吐量通常要高于ArrayBlockingQueue
。靜態工廠方法Executors.newFixedThreadPool()
使用了這個隊列。 -
SynchronousQueue
:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處于阻塞狀態,吞吐量通常要高于Linked-BlockingQueue
,靜態工廠方法Executors.newCachedThreadPool
使用了這個隊列。 -
PriorityBlockingQueue
:一個具有優先級的無限阻塞隊列。
-
-
handler
:飽和策略(又稱拒絕策略)。當隊列和線程池都滿了,說明線程池處于飽和狀態,那么必須采取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy
,表示無法處理新任務時拋出異常。在JDK 1.5
中 Java 線程池框架提供了以下4種策略。-
AbortPolicy
:直接拋出異常。 -
CallerRunsPolicy
:只用調用者所在線程來運行任務。 -
DiscardOldestPolicy
:丟棄隊列里最近的一個任務,并執行當前任務。 -
DiscardPolicy
:不處理,丟棄掉
-
歡迎關注公眾號 山間木匠 , 我是小春哥,從事 Java 后端開發,會一點前端、通過持續輸出系列技術文章以文會友,如果本文能為您提供幫助,歡迎大家關注、在看、 點贊、分享支持,我們下期再見!<br />