一篇部落格帶你輕鬆應對java面試中的多執行緒與高並發
1. Java執行緒的創建方式
(1)繼承thread類
thread類本質是實現了runnable介面的一個實例,代表執行緒的一個實例。啟動執行緒的方式start方法。start是一個本地方法,執行後,執行run方法的程式碼。

(2)實現runnable介面
如果自己的類已經繼承了別的類,就不能繼承thread類。只能實現runnable介面。

(3)實現callable介面
有返回值的任務必須實現callable介面,無返回值的任務必須實現runnable介面。執行callable介面後,可以獲取一個future對象,通過future對象的get方法可以獲得返回值。結合執行緒池可以實現有返回值的多執行緒。

(4)基於執行緒池的方式

2. 介紹一下java的執行緒池
java裡面執行緒池的頂級介面是executor。嚴格意義上講。executor只是一個介面,真正的執行緒池是executorservice。
(1)newCachedThreadPool
創建一個可根據需要創建新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。對於執行很多短期非同步任務的程式而言,這些執行緒池通常可提高程式性能。調用 execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則創建一個新執行緒並添加到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。因此,長時間保持空閑的執行緒池不會使用任何資源。
(2)newFixedThreadPool
創建一個可重用固定執行緒數的執行緒池,以共享的無界隊列方式來運行這些執行緒。在任意點,在大多數 nThreads 執行緒會處於處理任務的活動狀態。如果在所有執行緒處於活動狀態時提交附加任務,則在有可用執行緒之前,附加任務將在隊列中等待。如果在關閉前的執行期間由於失敗而導致任何執行緒終止,那麼一個新執行緒將代替它執行後續的任務(如果需要)。在某個執行緒被顯式地關閉之前,池中的執行緒將一直存在。
(3)newScheduledThreadPool
創建一個執行緒池,它可安排在給定延遲後運行命令或者定期地執行。

(4)newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一個執行緒池(這個執行緒池只有一個執行緒),這個執行緒
池可以在執行緒死後(或發生異常時)重新啟動一個執行緒來替代原來的執行緒繼續執行下去!
3. 執行緒的聲明周期
執行緒的生命周期包括新建new,就緒runable,運行running,阻塞blocked和死亡dead。
(1)新建狀態
當程式使用new關鍵字創建了一個執行緒之後,該執行緒就屬於新建狀態,此時僅由jvm為其分配記憶體,並初始化成員變數的值。
(2)就緒狀態
當執行緒對象調用了start方法之後。執行緒處於就緒狀態,jvm會為其創建方法調用棧和程式計數器。此時的現場等待cpu的調度。一旦拿到cpu就可以立即執行。
(3)運行狀態
處於就緒狀態的執行緒獲得了cpu的執行權,狀態就更改為running。此時執行緒處於運行狀態。
(4)阻塞狀態
阻塞狀態是指執行緒因為某種原因,放棄了cpu的使用權,暫時停止運行。恢復阻塞後進入就緒狀態,獲得cpu使用權之後,才進入執行狀態。
阻塞的情況分為三種:
等待阻塞
運行中的執行緒執行wait方法,jvm會把他放入等待隊列中。
同步阻塞
運行的執行緒獲取對象的同步鎖的時候,jvm會把該執行緒放入鎖池中。
其他阻塞
運行中的執行緒執行執行緒的sleep方法或join方法。或者發出io請求的時候,jvm把對象置為阻塞狀態。當sleep超時,join或者io完畢後,就可以拿到cpu的權,繼續執行。
(5)死亡狀態
正常結束,run或call的方法結束。
異常結束,出現報錯
調用stop,調用stop方法可能會產生思索。
4. 終止執行緒的四種方式
(1)正常執行結束
(2)使用同一標誌,多個執行緒共用一個變數,變數使用volite修飾,每次把他作為標誌位來進行判斷。
(3)interrupt結束執行緒
當執行緒處於阻塞狀態的時候,如果使用sleep,同步鎖的wait方法,socket的receive方法的時候,會使現場處於阻塞狀態。當調用執行緒的interrupt方法的時候。會拋出interruptexception異常。阻塞中的那個方法拋出異常,通過程式碼捕獲異常,然後結束執行。
執行緒未處於阻塞狀態的時候,可以使用isinterrupted來進行判斷,while來調這個函數。

(4)stop方法終止執行緒
stop方法強制執行,會導致現場釋放他所佔有的所有鎖、被保護的數據可能就會出現不一致性。可能會出現很多奇怪的應用程式錯誤。
5. sleep和wait方法的區別
對於sleep方法,屬於Thread類,wait方法數據object類中。
sleep方法導致執行緒的短暫執行,讓出cpu去執行其他執行緒。依然監控cpu,當時間到了,立馬拿到cpu的執行權。
調用sleep方法的時候,執行緒不會釋放鎖。wait方法會放棄對象鎖,進入鎖的等待池。此方法調用了notify之後,才能進入鎖池,進行重新競爭。
6. start與run方法的區別
start方法來啟動執行緒,真正實現了多執行緒運行。無需等待run方法結束。可以直接執行其他方法。
調用start方法使執行緒進入就緒狀態,獲得cpu即可運行。
run方法是執行緒的run方法執行體。
7. Java的後台進程
1. 定義:守護執行緒--也稱「服務執行緒」,他是後台執行緒,它有一個特性,即為用戶執行緒提供公
共服務,在沒有用戶執行緒可服務時會自動離開。
2. 優先順序:守護執行緒的優先順序比較低,用於為系統中的其它對象和執行緒提供服務。
3. 設置:通過 setDaemon(true)來設置執行緒為「守護執行緒」;將一個用戶執行緒設置為守護執行緒
的方式是在 執行緒對象創建 之前 用執行緒對象的 setDaemon 方法。
4. 在 Daemon 執行緒中產生的新執行緒也是 Daemon 的。
5. 執行緒則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個執行緒,這個執行緒的生命周期並不會和 Web 應用程式保持同步。也就是說,即使你停止了 Web 應用,這個執行緒依舊是活躍的。
6. example: 垃圾回收執行緒就是一個經典的守護執行緒,當我們的程式中不再有任何運行的Thread,程式就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收執行緒是 JVM 上僅剩的執行緒時,垃圾回收執行緒會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。
7. 生命周期:守護進程(Daemon)是運行在後台的一種特殊進程。它獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。也就是說守護執行緒不依賴於終端,但是依賴於系統,與系統「同生共死」。當 JVM 中所有的執行緒都是守護執行緒的時候,JVM 就可以退出了;如果還有一個或以上的非守護執行緒則 JVM 不會退出。
8. Java的鎖
樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到並發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。java 中的樂觀鎖基本都是通過 CAS 操作實現的,CAS 是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到並發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會 block 直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如 RetreenLock。
自旋鎖
自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖
的執行緒就不需要做內核態和用戶態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),
等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免用戶執行緒和內核的切換的消耗。
執行緒自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那執行緒也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖
的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態。
自旋鎖的優缺點
自旋鎖儘可能的減少執行緒的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間非常短的程式碼塊來
說性能能大幅度的提升,因為自旋的消耗會小於執行緒阻塞掛起再喚醒的操作的消耗,這些操作會導致執行緒發生兩次上下文切換!
但是如果鎖的競爭激烈,或者持有鎖的執行緒需要長時間佔用鎖執行同步塊,這時候就不適合
使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是佔用 cpu 做無用功,占著 XX 不 XX,同時有大量執行緒在競爭一個鎖,會導致獲取鎖的時間很長,執行緒自旋的消耗大於執行緒阻塞掛起操作的消耗,其它需要 cup 的執行緒又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值
自旋鎖的目的是為了占著 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的執行緒處於自旋狀態佔用 CPU 資源,進而會影響整體系統的性能。因此自旋的周期選的額外重要!
JVM 對於自旋周期的選擇,jdk1.5 這個限度是一定的寫死的,在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認為一個執行緒上下文切換的時間是最佳的一個時間,同時 JVM 還針對當前 CPU 的負荷情況做了較多的優化,如果平均負載小於 CPUs 則一直自旋,如果有超過(CPUs/2)個執行緒正在自旋,則後來執行緒直接阻塞,如果正在自旋的執行緒發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞,如果 CPU 處於節電模式則停止自旋,自旋時間的最壞情況是 CPU的存儲延遲(CPU A 存儲了一個數據,到 CPU B 得知這個數據直接的時間差),自旋時會適當放棄執行緒優先順序之間的差異。

Synchronized 同步鎖
synchronized 它可以把任意一個非 NULL 的對象當作鎖。他屬於獨佔式的悲觀鎖,同時屬於可重入鎖。
Synchronized 作用範圍
1. 作用於方法時,鎖住的是對象的實例(this);
2. 當作用於靜態方法時,鎖住的是Class實例,又因為Class的相關數據存儲在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全局共享的,因此靜態方法鎖相當於類的一個全局鎖,會鎖所有調用該方法的執行緒;
3. synchronized 作用於一個對象實例時,鎖住的是所有以該對象為鎖的程式碼塊。它有多個隊列,當多個執行緒一起訪問某個對象監視器的時候,對象監視器會將這些執行緒存儲在不同的容器中。
Synchronized 核心組件
1) Wait Set:哪些調用 wait 方法被阻塞的執行緒被放置在這裡;
2) Contention List:競爭隊列,所有請求鎖的執行緒首先被放在這個競爭隊列中;
3) Entry List:Contention List 中那些有資格成為候選資源的執行緒被移動到 Entry List 中;
4) OnDeck:任意時刻,最多只有一個執行緒正在競爭鎖資源,該執行緒被成為 OnDeck;
5) Owner:當前已經獲取到所資源的執行緒被稱為 Owner;
6) !Owner:當前釋放鎖的執行緒。
1. JVM 每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是並發情況下,
ContentionList 會被大量的並發執行緒進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將一部分執行緒移動到 EntryList 中作為候選競爭執行緒。
2. Owner 執行緒會在 unlock 時,將 ContentionList 中的部分執行緒遷移到 EntryList 中,並指定EntryList 中的某個執行緒為 OnDeck 執行緒(一般是最先進去的那個執行緒)。
3. Owner 執行緒並不直接把鎖傳遞給 OnDeck 執行緒,而是把鎖競爭的權利交給 OnDeck,
OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM 中,也把這種選擇行為稱之為「競爭切換」。
4. OnDeck 執行緒獲取到鎖資源後會變為 Owner 執行緒,而沒有得到鎖資源的仍然停留在 EntryList中。如果 Owner 執行緒被 wait 方法阻塞,則轉移到 WaitSet 隊列中,直到某個時刻通過 notify或者 notifyAll 喚醒,會重新進去 EntryList 中。
5. 處於 ContentionList、EntryList、WaitSet 中的執行緒都處於阻塞狀態,該阻塞是由作業系統
來完成的(Linux 內核下採用 pthread_mutex_lock 內核函數實現的)。
6. Synchronized 是非公平鎖。 Synchronized 在執行緒進入 ContentionList 時,等待的執行緒會先嘗試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對於已經進入隊列的執行緒是不公平的,還有一個不公平的事情就是自旋獲取鎖的執行緒還可能直接搶佔 OnDeck 執行緒的鎖資源。
參考://blog.csdn.net/zqz_zqz/article/details/70233767
7. 每個對象都有個 monitor 對象,加鎖就是在競爭 monitor 對象,程式碼塊加鎖是在前後分別加上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的
8. synchronized 是一個重量級操作,需要調用作業系統相關介面,性能是低效的,有可能給執行緒加鎖消耗的時間比有用操作消耗的時間更多。
9. Java1.6,synchronized 進行了很多的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之後推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做了優化。引入了偏向鎖和輕量級鎖。都是在對象頭中有標記位,不需要經過作業系統加鎖。
ReentrantLock
ReentantLock 繼承介面 Lock 並實現了介面中定義的方法,他是一種可重入鎖,除了能完
成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多執行緒死鎖的方法。
Lock 介面的主要方法
1. void lock(): 執行此方法時, 如果鎖處於空閑狀態, 當前執行緒將獲取到鎖. 相反, 如果鎖已經被其他執行緒持有, 將禁用當前執行緒, 直到當前執行緒獲取到鎖.
2. boolean tryLock():如果鎖可用, 則獲取鎖, 並立即返回 true, 否則返回 false. 該方法和
lock()的區別在於, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前執行緒被禁用,
當前執行緒仍然繼續往下執行程式碼. 而 lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一
直等待, 在未獲得鎖之前,當前執行緒並不繼續向下執行.
3. void unlock():執行此方法時, 當前執行緒將釋放持有的鎖. 鎖只能由持有者釋放, 如果執行緒
並不持有鎖, 卻執行該方法, 可能導致異常的發生.
4. Condition newCondition():條件對象,獲取等待通知組件。該組件和當前的鎖綁定,
當前執行緒只有獲取了鎖,才能調用該組件的 await()方法,而調用後,當前執行緒將縮放鎖。
5. getHoldCount() :查詢當前執行緒保持此鎖的次數,也就是執行此執行緒執行 lock 方法的次
數。
6. getQueueLength():返回正等待獲取此鎖的執行緒估計數,比如啟動 10 個執行緒,1 個
執行緒獲得鎖,此時返回的是 9
7. getWaitQueueLength:(Condition condition)返回等待與此鎖相關的給定條件的線
程估計數。比如 10 個執行緒,用同一個 condition 對象,並且此時這 10 個執行緒都執行了
condition 對象的 await 方法,那麼此時執行此方法返回 10
8. hasWaiters(Condition condition):查詢是否有執行緒等待與此鎖有關的給定條件
(condition),對於指定 contidion 對象,有多少執行緒執行了 condition.await 方法
9. hasQueuedThread(Thread thread):查詢給定執行緒是否等待獲取此鎖
10. hasQueuedThreads():是否有執行緒等待此鎖
11. isFair():該鎖是否公平鎖
12. isHeldByCurrentThread(): 當前執行緒是否保持鎖鎖定,執行緒的執行 lock 方法的前後分
別是 false 和 true
13. isLock():此鎖是否有任意執行緒佔用
14. lockInterruptibly():如果當前執行緒未被中斷,獲取鎖
15. tryLock():嘗試獲得鎖,僅在調用時鎖未被執行緒佔用,獲得鎖
16. tryLock(long timeout TimeUnit unit):如果鎖在給定等待時間內沒有被另一個執行緒保持,
則獲取該鎖
非公平鎖
JVM 按隨機、就近原則分配鎖的機制則稱為不公平鎖,ReentrantLock 在構造函數中提供了
是否公平鎖的初始化方式,默認為非公平鎖。非公平鎖實際執行的效率要遠遠超出公平鎖,除非程式有特殊需要,否則最常用非公平鎖的分配機制。
公平鎖
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的執行緒會先被分配到鎖,ReentrantLock 在構造函數中提供了是否公平鎖的初始化方式來定義公平鎖。
ReentrantLock 與 synchronized
1. ReentrantLock 通過方法 lock()與 unlock()來進行加鎖與解鎖操作,與 synchronized 會被 JVM 自動解鎖機制不同,ReentrantLock 加鎖後需要手動進行解鎖。為了避免程式出現異常而無法正常解鎖的情況,使用 ReentrantLock 必須在 finally 控制塊中進行解鎖操作。
2. ReentrantLock 相比 synchronized 的優勢是可中斷、公平鎖、多個鎖。這種情況下需要
使用 ReentrantLock。

Condition 類和 Object 類鎖方法區別區別
1. Condition 類的 awiat 方法和 Object 類的 wait 方法等效
2. Condition 類的 signal 方法和 Object 類的 notify 方法等效
3. Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
4. ReentrantLock 類可以喚醒指定條件的執行緒,而 object 的喚醒是隨機的
指定條件喚醒,多建立幾個condition。
tryLock 和 lock 和 lockInterruptibly 的區別
1. tryLock 能獲得鎖就返回 true,不能就立即返回 false,tryLock(long timeout,TimeUnit
unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false。
2. lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖。
3. lock 和 lockInterruptibly,如果兩個執行緒分別執行這兩個方法,但此時中斷這兩個執行緒,
lock 不會拋出異常,而 lockInterruptibly 會拋出異常。
可重入鎖的好處
假如一個執行緒擁有了這個鎖。另一個執行緒需要這個鎖,這個時候進行調用。可以直接調用,不用等待重新獲取。
Semaphore 訊號量
Semaphore 是一種基於計數的訊號量。它可以設定一個閾值,基於此,多個執行緒競爭獲取許可訊號,做完自己的申請後歸還,超過閾值後,執行緒申請許可訊號將會被阻塞。Semaphore 可以用來構建一些對象池,資源池之類的,比如資料庫連接池。
實現互斥鎖(計數器為 1)
我們也可以創建計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元訊號量,表示兩種互斥狀態。
程式碼實現
其他用途
可以創建一個訊號量,每個執行緒消耗一下訊號量。用完之後。獲取一下剩餘數量,如果和初始相等,證明執行緒內部都執行完畢了,可以繼續執行主執行緒了。
Semaphore 與 ReentrantLock
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也與之類似,通過 acquire()與release()方法來獲得和釋放臨界資源。經實測,Semaphone.acquire()方法默認為可響應中斷鎖,與 ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外,Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock不同,其使用方法與 ReentrantLock 幾乎一致。Semaphore 也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。
Semaphore 的鎖釋放操作也由手動進行,因此與 ReentrantLock 一樣,為避免執行緒因拋出異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 程式碼塊中完成。
AtomicInteger
首先說明,此處 AtomicInteger ,一個提供原子操作的 Integer 的類,常見的還有
AtomicBoolean、AtomicInteger、AtomicLong、AtomicReference 等,他們的實現原理相同,
區別在與運算對象類型的不同。令人興奮地,還可以通過 AtomicReference<V>將一個對象的所有操作轉化成原子操作。
我們知道,在多執行緒程式中,諸如++i 或 i++等運算不具有原子性,是不安全的執行緒操作之一。
通常我們會使用 synchronized 將該操作變成一個原子操作,但 JVM 為此類操作特意提供了一些同步類,使得使用更方便,且使程式運行效率變得更高。通過相關資料顯示,通常AtomicInteger的性能是 ReentantLock 的好幾倍。
可重入鎖(遞歸鎖)
本文裡面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。可重入鎖,也叫做遞歸鎖,指的是同一執行緒 外層函數獲得鎖之後 ,內層遞歸函數仍然有獲取該鎖的程式碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是可重入鎖。
公平鎖與非公平鎖
公平鎖(Fair)
加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
1. 非公平鎖性能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個隊列
2. Java 中的 synchronized 是非公平鎖,ReentrantLock 默認的 lock()方法採用的是非公平鎖。
ReadWriteLock 讀寫鎖
為了提高性能,Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程式的執行效率。讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可。
讀鎖
如果你的程式碼只讀數據,可以很多人同時讀,但不能同時寫,那就上讀鎖。
寫鎖
如果你的程式碼修改數據,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
Java 中讀寫鎖有個介面 java.util.concurrent.locks.ReadWriteLock ,也有具體的實現
ReentrantReadWriteLock。
共享鎖和獨佔鎖
獨佔鎖
獨佔鎖模式下,每次只能有一個執行緒能持有鎖,ReentrantLock 就是以獨佔方式實現的互斥鎖。
獨佔鎖是一種悲觀保守的加鎖策略,它避免了讀/讀衝突,如果某個只讀執行緒獲取鎖,則其他讀執行緒都只能等待,這種情況下就限制了不必要的並發性,因為讀操作並不會影響數據的一致性。
共享鎖
共享鎖則允許多個執行緒同時獲取鎖,並發訪問 共享資源,如:ReadWriteLock。共享鎖則是一種樂觀鎖,它放寬了加鎖策略,允許多個執行讀操作的執行緒同時訪問共享資源。
1. AQS 的內部類 Node 定義了兩個常量 SHARED 和 EXCLUSIVE,他們分別標識 AQS 隊列中等待執行緒的鎖獲取模式。
2. java 的並發包中提供了 ReadWriteLock,讀-寫鎖。它允許一個資源可以被多個讀操作訪問,
或者被一個寫操作訪問,但兩者不能同時進行。
重量級鎖(Mutex Lock)
Synchronized 是通過對象內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴於底層的作業系統的 Mutex Lock 來實現的。而作業系統實現執行緒之間的切換這就需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼Synchronized 效率低的原因。因此,這種依賴於作業系統 Mutex Lock 所實現的鎖我們稱之為「重量級鎖」。JDK 中對 Synchronized 做的種種優化,其核心都是為了減少這種重量級鎖的使用。
JDK1.6 以後,為了減少獲得鎖和釋放鎖所帶來的性能消耗,提高性能,引入了「輕量級鎖」和「偏向鎖」。
輕量級鎖
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
鎖升級
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。
「輕量級」是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
偏向鎖
Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。偏向鎖的目的是在某個執行緒獲得鎖之後,消除這個執行緒鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏護。引入偏向鎖是為了在無多執行緒競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換ThreadID 的時候依賴一次 CAS 原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的 CAS 原子指令的性能消耗)。上面說過,輕量級鎖是為了在執行緒交替執行同步塊時提高性能,而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高性能。
分段鎖
分段鎖也並非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐。
鎖優化
減少鎖持有時間
只用在有執行緒安全要求的程式上加鎖
減小鎖粒度
將大對象(這個對象可能會被很多執行緒訪問),拆成小對象,大大增加並行度,降低鎖競爭。
降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。最最典型的減小鎖粒度的案例就是
ConcurrentHashMap。
鎖分離
最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了執行緒安全,又提高了性能。JDK 並發包 1。讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。比如LinkedBlockingQueue 從頭部取出,從尾部放數據。
鎖粗化
通常情況下,為了保證多執行緒間的有效並發,會要求每個執行緒持有鎖的時間盡量短,即在使用完公共資源後,應該立即釋放鎖。但是,凡事都有一個度,如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化 。
鎖消除
鎖消除是在編譯器級別的事情。在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作,多數是因為程式設計師編碼不規範引起。
9. 執行緒基本方法

執行緒等待(wait)
調用該方法的執行緒進入 WAITING 狀態,只有等待另外執行緒的通知或被中斷才會返回,需要注意的是調用 wait()方法後,會釋放對象的鎖。因此,wait 方法一般用在同步方法或同步程式碼塊中。
執行緒睡眠(sleep)
sleep 導致當前執行緒休眠,與 wait 方法不同的是 sleep 不會釋放當前佔有的鎖,sleep(long)會導致執行緒進入 TIMED-WATING 狀態,而 wait()方法會導致當前執行緒進入 WATING 狀態。
執行緒讓步(yield)
yield 會使當前執行緒讓出 CPU 執行時間片,與其他執行緒一起重新競爭 CPU 時間片。一般情況下,優先順序高的執行緒有更大的可能性成功競爭得到 CPU 時間片,但這又不是絕對的,有的作業系統對執行緒優先順序並不敏感。
執行緒中斷(interrupt)
中斷一個執行緒,其本意是給這個執行緒一個通知訊號,會影響這個執行緒內部的一個中斷標識位。這個執行緒本身並不會因此而改變狀態(如阻塞,終止等)。
1. 調用 interrupt()方法並不會中斷一個正在運行的執行緒。也就是說處於 Running 狀態的線
程並不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。
2. 若調用 sleep()而使執行緒處於 TIMED-WATING 狀態,這時調用 interrupt()方法,會拋出
InterruptedException,從而使執行緒提前結束 TIMED-WATING 狀態。
3. 許多聲明拋出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),拋出異
常前,都會清除中斷標識位,所以拋出異常後,調用 isInterrupted()方法將會返回 false。
4. 中斷狀態是執行緒固有的一個標識位,可以通過此標識位安全的終止執行緒。比如,你想終止
一個執行緒 thread 的時候,可以調用 thread.interrupt()方法,在執行緒的 run 方法內部可以
根據 thread.isInterrupted()的值來優雅的終止執行緒。
Join 等待其他執行緒終止
join() 方法,等待其他執行緒終止,在當前執行緒中調用一個執行緒的 join() 方法,則當前執行緒轉為阻塞狀態,回到另一個執行緒結束,當前執行緒再由阻塞狀態變為就緒狀態,等待 cpu 的寵幸。
為什麼要用 join()方法?
很多情況下,主執行緒生成並啟動了子執行緒,需要用到子執行緒返回的結果,也就是需要主執行緒需要在子執行緒結束後再結束,這時候就要用到 join() 方法。

執行緒喚醒(notify)
Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個執行緒,如果所有執行緒都在此對象上等待,則會選擇喚醒其中一個執行緒,選擇是任意的,並在對實現做出決定時發生,執行緒通過調用其中一個 wait() 方法,在對象的監視器上等待,直到當前的執行緒放棄此對象上的鎖定,才能繼續執行被喚醒的執行緒,被喚醒的執行緒將以常規方式與在該對象上主動同步的其他所有執行緒進行競爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有執行緒。
其他方法
1. sleep():強迫一個執行緒睡眠N毫秒。
2. isAlive(): 判斷一個執行緒是否存活。
3. join(): 等待執行緒終止。
4. activeCount(): 程式中活躍的執行緒數。
5. enumerate(): 枚舉程式中的執行緒。
6. currentThread(): 得到當前執行緒。
7. isDaemon(): 一個執行緒是否為守護執行緒。
8. setDaemon(): 設置一個執行緒為守護執行緒。(用戶執行緒和守護執行緒的區別在於,是否等待主線
程依賴於主執行緒結束而結束)
9. setName(): 為執行緒設置一個名稱。
10. wait(): 強迫一個執行緒等待。
11. notify(): 通知一個執行緒繼續運行。
12. setPriority(): 設置一個執行緒的優先順序。
13. getPriority()::獲得一個執行緒的優先順序。
10. 執行緒的上下文切換
巧妙地利用了時間片輪轉的方式, CPU 給每個任務都服務一定的時間,然後把當前任務的狀態保存下來,在載入下一任務的狀態後,繼續服務下一任務,任務的狀態保存及再載入, 這段過程就叫做上下文切換。時間片輪轉的方式使多個任務在同一顆 CPU 上執行變成了可能。
線程
(有時候也稱做任務)是指一個程式運行的實例。在 Linux 系統中,執行緒就是能並行運行並且與他們的父進程(創建他們的進程)共享同一地址空間(一段記憶體區域)和其他資源的輕量級的進程。
上下文
是指某一時間點 CPU 暫存器和程式計數器的內容。
暫存器
是 CPU 內部的數量較少但是速度很快的記憶體(與之對應的是 CPU 外部相對較慢的 RAM 主記憶體)。暫存器通過對常用值(通常是運算的中間值)的快速訪問來提高電腦程式運行的速度。
程式計數器
是一個專用的暫存器,用於表明指令序列中 CPU 正在執行的位置,存的值為正在執行的指令的位置或者下一個將要被執行的指令的位置,具體依賴於特定的系統。
PCB-「切換楨
上下文切換可以認為是內核(作業系統的核心)在 CPU 上對於進程(包括執行緒)進行切換,上下文切換過程中的資訊是保存在進程式控制制塊(PCB, process control block)中的。PCB 還經常被稱作「切換楨」(switchframe)。資訊會一直保存到 CPU 的記憶體中,直到他們被再次使用。
上下文切換的活動
1. 掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於記憶體中的某處。
2. 在記憶體中檢索下一個進程的上下文並將其在 CPU 的暫存器中恢復。
3. 跳轉到程式計數器所指向的位置(即跳轉到進程被中斷時的程式碼行),以恢復該進程在程式中。
引起執行緒上下文切換的原因
1. 當前執行任務的時間片用完之後,系統 CPU 正常調度下一個任務;
2. 當前執行任務碰到 IO 阻塞,調度器將此任務掛起,繼續下一任務;
3. 多個任務搶佔鎖資源,當前任務沒有搶到鎖資源,被調度器掛起,繼續下一任務;
4. 用戶程式碼掛起當前任務,讓出 CPU 時間;
5. 硬體中斷;
11. 同步鎖與死鎖
同步鎖
當多個執行緒同時訪問同一個數據時,很容易出現問題。為了避免這種情況出現,我們要保證執行緒同步互斥,就是指並發執行的多個執行緒,在同一時間內只允許一個執行緒訪問共享數據。 Java 中可以使用 synchronized 關鍵字來取得一個對象的同步鎖。
死鎖
何為死鎖,就是多個執行緒同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。
12. 執行緒池原理
執行緒池做的工作主要是控制運行的執行緒的數量,處理過程中將任務放入隊列,然後在執行緒創建後啟動這些任務,如果執行緒數量超過了最大數量超出數量的執行緒排隊等候,等其它執行緒執行完畢,再從隊列中取出任務來執行。他的主要特點為:執行緒復用;控制最大並發數;管理執行緒。
執行緒復用
每一個 Thread 的類都有一個 start 方法。 當調用 start 啟動執行緒時 Java 虛擬機會調用該類的 run 方法。 那麼該類的 run() 方法中就是調用了 Runnable 對象的 run() 方法。 我們可以繼承重寫Thread 類,在其 start 方法中添加不斷循環調用傳遞過來的 Runnable 對象。 這就是執行緒池的實現原理。循環方法中不斷獲取 Runnable 是用 Queue 實現的,在獲取下一個 Runnable 之前可以是阻塞的。
執行緒池的組成
一般的執行緒池主要分為以下 4 個組成部分:
1. 執行緒池管理器:用於創建並管理執行緒池
2. 工作執行緒:執行緒池中的執行緒
3. 任務介面:每個任務必須實現的介面,用於工作執行緒調度其運行
4. 任務隊列:用於存放待處理的任務,提供一種緩衝機制
Java 中的執行緒池是通過 Executor 框架實現的,該框架中用到了 Executor,Executors,
ExecutorService,ThreadPoolExecutor ,Callable 和 Future、FutureTask 這幾個類。
ThreadPoolExecutor 的構造方法如下:
1. corePoolSize:指定了執行緒池中的執行緒數量。
2. maximumPoolSize:指定了執行緒池中的最大執行緒數量。
3. keepAliveTime:當前執行緒池數量超過 corePoolSize 時,多餘的空閑執行緒的存活時間,即多
次時間內會被銷毀。
4. unit:keepAliveTime 的單位。
5. workQueue:任務隊列,被提交但尚未被執行的任務。
6. threadFactory:執行緒工廠,用於創建執行緒,一般用默認的即可。
7. handler:拒絕策略,當任務太多來不及處理,如何拒絕任務。
拒絕策略
執行緒池中的執行緒已經用完了,無法繼續為新任務服務,同時,等待隊列也已經排滿了,再也
塞不下新任務了。這時候我們就需要拒絕策略機制合理的處理這個問題。
JDK 內置的拒絕策略如下:
1. AbortPolicy : 直接拋出異常,阻止系統正常運行。
2. CallerRunsPolicy : 只要執行緒池未關閉,該策略直接在調用者執行緒中,運行當前被丟棄的
任務。顯然這樣做不會真的丟棄任務,但是,任務提交執行緒的性能極有可能會急劇下降。
3. DiscardOldestPolicy : 丟棄最老的一個請求,也就是即將被執行的一個任務,並嘗試再
次提交當前任務。
4. DiscardPolicy : 該策略默默地丟棄無法處理的任務,不予任何處理。如果允許任務丟
失,這是最好的一種方案。
以上內置拒絕策略均實現了 RejectedExecutionHandler 介面,若以上策略仍無法滿足實際
需要,完全可以自己擴展 RejectedExecutionHandler 介面。
Java 執行緒池工作過程
1. 執行緒池剛創建時,裡面沒有一個執行緒。任務隊列是作為參數傳進來的。不過,就算隊列裡面有任務,執行緒池也不會馬上執行它們。
2. 當調用 execute() 方法添加一個任務時,執行緒池會做如下判斷:
a) 如果正在運行的執行緒數量小於 corePoolSize,那麼馬上創建執行緒運行這個任務;
b) 如果正在運行的執行緒數量大於或等於 corePoolSize,那麼將這個任務放入隊列;
c) 如果這時候隊列滿了,而且正在運行的執行緒數量小於 maximumPoolSize,那麼還是要
創建非核心執行緒立刻運行這個任務;
d) 如果隊列滿了,而且正在運行的執行緒數量大於或等於 maximumPoolSize,那麼執行緒池
會拋出異常 RejectExecutionException。
3. 當一個執行緒完成任務時,它會從隊列中取下一個任務來執行。
4. 當一個執行緒無事可做,超過一定的時間(keepAliveTime)時,執行緒池會判斷,如果當前運行的執行緒數大於 corePoolSize,那麼這個執行緒就被停掉。所以執行緒池的所有任務完成後,它最終會收縮到 corePoolSize 的大小。
13. JAVA 阻塞隊列原理
阻塞隊列,關鍵字是阻塞,先理解阻塞的含義,在阻塞隊列中,執行緒阻塞有這樣的兩種情況:
- 當隊列中沒有數據的情況下,消費者端的所有執行緒都會被自動阻塞(掛起),直到有數據放入隊列。
- 當隊列中填滿數據的情況下,生產者端的所有執行緒都會被自動阻塞(掛起),直到隊列中有空的位置,執行緒被自動喚醒。
阻塞隊列的主要方法

„ 拋出異常:拋出一個異常;
„ 特殊值:返回一個特殊值(null 或 false,視情況而定)
„ 則塞:在成功操作之前,一直阻塞執行緒
„ 超時:放棄前只在最大的時間內阻塞
插入操作:
1:public abstract boolean add(E paramE):將指定元素插入此隊列中(如果立即可行
且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則拋
出 IllegalStateException。如果該元素是 NULL,則會拋出 NullPointerException 異常。
2:public abstract boolean offer(E paramE):將指定元素插入此隊列中(如果立即可行
且不會違反容量限制),成功時返回 true,如果當前沒有可用的空間,則返回 false。
3:public abstract void put(E paramE) throws InterruptedException: 將指定元素插入此隊列中,將等待可用的空間(如果有必要)
public void put(E paramE) throws InterruptedException {
checkNotNull(paramE);
ReentrantLock localReentrantLock = this.lock;
localReentrantLock.lockInterruptibly();
try {
while (this.count == this.items.length)
this.notFull.await();//如果隊列滿了,則執行緒阻塞等待
enqueue(paramE);
localReentrantLock.unlock();
} finally {
localReentrantLock.unlock();
}
}
4:offer(E o, long timeout, TimeUnit unit):可以設定等待的時間,如果在指定的時間
內,還不能往隊列中加入 BlockingQueue,則返回失敗。
獲取數據操作:
1:poll(time):取走 BlockingQueue 里排在首位的對象,若不能立即取出,則可以等 time 參數
規定的時間,取不到時返回 null;
2:poll(long timeout, TimeUnit unit):從 BlockingQueue 取出一個隊首的對象,如果在
指定時間內,隊列一旦有數據可取,則立即返回隊列中的數據。否則直到時間超時還沒有數
據可取,返回失敗。
3:take():取走 BlockingQueue 里排在首位的對象,若 BlockingQueue 為空,阻斷進入等待狀
態直到 BlockingQueue 有新的數據被加入。
4.drainTo():一次性從 BlockingQueue 獲取所有可用的數據對象(還可以指定獲取數據的個
數),通過該方法,可以提升獲取數據效率;不需要多次分批加鎖或釋放鎖。
Java 中的阻塞隊列
1. ArrayBlockingQueue :由數組結構組成的有界阻塞隊列。
2. LinkedBlockingQueue :由鏈表結構組成的有界阻塞隊列。
3. PriorityBlockingQueue :支援優先順序排序的無界阻塞隊列。
4. DelayQueue:使用優先順序隊列實現的無界阻塞隊列。
5. SynchronousQueue:不存儲元素的阻塞隊列。
6. LinkedTransferQueue:由鏈表結構組成的無界阻塞隊列。
7. LinkedBlockingDeque:由鏈表結構組成的雙向阻塞隊列
ArrayBlockingQueue(公平、非公平)
用數組實現的有界阻塞隊列。此隊列按照先進先出(FIFO)的原則對元素進行排序。默認情況下不保證訪問者公平的訪問隊列,所謂公平訪問隊列是指阻塞的所有生產者執行緒或消費者執行緒,當隊列可用時,可以按照阻塞的先後順序訪問隊列,即先阻塞的生產者執行緒,可以先往隊列里插入元素,先阻塞的消費者執行緒,可以先從隊列里獲取元素。通常情況下為了保證公平性會降低吞吐量。我們可以使用以下程式碼創建一個公平的阻塞隊列:
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
LinkedBlockingQueue(兩個獨立鎖提高並發)
基於鏈表的阻塞隊列,同 ArrayListBlockingQueue 類似,此隊列按照先進先出(FIFO)的原則對元素進行排序。而 LinkedBlockingQueue 之所以能夠高效的處理並發數據,還因為其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味著在高並發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的並發性能。
LinkedBlockingQueue 會默認一個類似無限大小的容量(Integer.MAX_VALUE)。
PriorityBlockingQueue(compareTo 排序實現優先)
是一個支援優先順序的無界隊列。默認情況下元素採取自然順序升序排列。可以自定義實現
compareTo()方法來指定元素進行排序規則,或者初始化 PriorityBlockingQueue 時,指定構造參數 Comparator 來對元素進行排序。需要注意的是不能保證同優先順序元素的順序。
DelayQueue(快取失效、定時任務 )
是一個支援延時獲取元素的無界阻塞隊列。隊列使用 PriorityQueue 來實現。隊列中的元素必須實現 Delayed 介面,在創建元素時可以指定多久才能從隊列中獲取當前元素。只有在延遲期滿時才能從隊列中提取元素。我們可以將 DelayQueue 運用在以下應用場景:
1. 快取系統的設計:可以用 DelayQueue 保存快取元素的有效期,使用一個執行緒循環查詢
DelayQueue,一旦能從 DelayQueue 中獲取元素時,表示快取有效期到了。
2. 定時任務調度:使用 DelayQueue 保存當天將會執行的任務和執行時間,一旦從
DelayQueue 中獲取到任務就開始執行,從比如 TimerQueue 就是使用 DelayQueue 實現的。
SynchronousQueue(不存儲數據、可用於傳遞數據)
是一個不存儲元素的阻塞隊列。每一個 put 操作必須等待一個 take 操作,否則不能繼續添加元素。
SynchronousQueue 可以看成是一個傳球手,負責把生產者執行緒處理的數據直接傳遞給消費者執行緒。隊列本身並不存儲任何元素,非常適合於傳遞性場景,比如在一個執行緒中使用的數據,傳遞給另外一個執行緒使用, SynchronousQueue 的吞吐量高於 LinkedBlockingQueue 和
ArrayBlockingQueue。
LinkedTransferQueue
是一個由鏈表結構組成的無界阻塞 TransferQueue 隊列。相對於其他阻塞隊列,
LinkedTransferQueue 多了 tryTransfer 和 transfer 方法。
1. transfer 方法:如果當前有消費者正在等待接收元素(消費者使用 take()方法或帶時間限制的poll()方法時),transfer 方法可以把生產者傳入的元素立刻 transfer(傳輸)給消費者。如果沒有消費者在等待接收元素,transfer 方法會將元素存放在隊列的 tail 節點,並等到該元素被消費者消費了才返回。
2. tryTransfer 方法。則是用來試探下生產者傳入的元素是否能直接傳給消費者。如果沒有消費者等待接收元素,則返回 false。和 transfer 方法的區別是 tryTransfer 方法無論消費者是否接收,方法立即返回。而 transfer 方法是必須等到消費者消費了才返回。
對於帶有時間限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法,則是試圖把生產者傳
入的元素直接傳給消費者,但是如果沒有消費者消費該元素則等待指定的時間再返回,如果超時還沒消費元素,則返回 false,如果在超時時間內消費了元素,則返回 true。
LinkedBlockingDeque
是一個由鏈表結構組成的雙向阻塞隊列。所謂雙向隊列指的你可以從隊列的兩端插入和移出元素。雙端隊列因為多了一個操作隊列的入口,在多執行緒同時入隊時,也就減少了一半的競爭。相比其他的阻塞隊列,LinkedBlockingDeque 多了 addFirst,addLast,offerFirst,offerLast,
peekFirst,peekLast 等方法,以 First 單詞結尾的方法,表示插入,獲取(peek)或移除雙端隊列的第一個元素。以 Last 單詞結尾的方法,表示插入,獲取或移除雙端隊列的最後一個元素。另外插入方法 add 等同於 addLast,移除方法 remove 等效於 removeFirst。但是 take 方法卻等同於 takeFirst,不知道是不是 Jdk 的 bug,使用時還是用帶有 First 和 Last 後綴的方法更清楚。
在初始化 LinkedBlockingDeque 時可以設置容量防止其過渡膨脹。另外雙向阻塞隊列可以運用在「工作竊取」模式中。
14. CyclicBarrier、CountDownLatch、Semaphore 的用法
CountDownLatch(執行緒計數器)
CountDownLatch 類位於 java.util.concurrent 包下,利用它可以實現類似計數器的功能。比如有一個任務 A,它要等待其他 4 個任務執行完畢之後才能執行,此時就可以利用 CountDownLatch來實現這種功能了。

CyclicBarrier(迴環柵欄-等待至 barrier 狀態再全部同時執行)
字面意思迴環柵欄,通過它可以實現讓一組執行緒等待至某個狀態之後再全部同時執行。叫做迴環是因為當所有等待執行緒都被釋放以後,CyclicBarrier 可以被重用。我們暫且把這個狀態就叫做barrier,當調用 await()方法之後,執行緒就處於 barrier 了。
CyclicBarrier 中最重要的方法就是 await 方法,它有 2 個重載版本:
1. public int await():用來掛起當前執行緒,直至所有執行緒都到達 barrier 狀態再同時執行後續任務;
2. public int await(long timeout, TimeUnit unit):讓這些執行緒等待至一定的時間,如果還有
執行緒沒有到達 barrier 狀態就直接讓到達 barrier 的執行緒執行後續任務。
具體使用如下,另外 CyclicBarrier 是可以重用的。

Semaphore(訊號量-控制同時訪問的執行緒個數)
Semaphore 翻譯成字面意思為訊號量,Semaphore 可以控制同時訪問的執行緒個數,通過
acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。
Semaphore 類中比較重要的幾個方法:
1. public void acquire(): 用來獲取一個許可,若無許可能夠獲得,則會一直等待,直到獲得許
可。
2. public void acquire(int permits):獲取 permits 個許可
3. public void release() { } :釋放許可。注意,在釋放許可之前,必須先獲獲得許可。
4. public void release(int permits) { }:釋放 permits 個許可
上面 4 個方法都會被阻塞,如果想立即得到執行結果,可以使用下面幾個方法
1. public boolean tryAcquire():嘗試獲取一個許可,若獲取成功,則立即返回 true,若獲取失
敗,則立即返回 false
2. public boolean tryAcquire(long timeout, TimeUnit unit):嘗試獲取一個許可,若在指定的
時間內獲取成功,則立即返回 true,否則則立即返回 false
3. public boolean tryAcquire(int permits):嘗試獲取 permits 個許可,若獲取成功,則立即返
回 true,若獲取失敗,則立即返回 false
4. public boolean tryAcquire(int permits, long timeout, TimeUnit unit): 嘗試獲取 permits
個許可,若在指定的時間內獲取成功,則立即返回 true,否則則立即返回 false
5. 還可以通過 availablePermits()方法得到可用的許可數目。
例子:若一個工廠有 5 台機器,但是有 8 個工人,一台機器同時只能被一個工人使用,只有使用完
了,其他工人才能繼續使用。那麼我們就可以通過 Semaphore 來實現:

CountDownLatch 和 CyclicBarrier 都能夠實現執行緒之間的等待,只不過它們側重點不
同;CountDownLatch 一般用於某個執行緒 A 等待若干個其他執行緒執行完任務之後,它才
執行;而 CyclicBarrier 一般用於一組執行緒互相等待至某個狀態,然後這一組執行緒再同時
執行;另外,CountDownLatch 是不能夠重用的,而 CyclicBarrier 是可以重用的。
Semaphore 其實和鎖有點類似,它一般用於控制對某組資源的訪問許可權。
15. volatile 關鍵字的作用(變數可見性、禁止重排序)
Java 語言提供了一種稍弱的同步機制,即 volatile 變數,用來確保將變數的更新操作通知到其他執行緒。volatile 變數具備兩種特性,volatile 變數不會被快取在暫存器或者對其他處理器不可見的地方,因此在讀取 volatile 類型的變數時總會返回最新寫入的值。
變數可見性
其一是保證該變數對所有執行緒可見,這裡的可見性指的是當一個執行緒修改了變數的值,那麼新的值對於其他執行緒是可以立即獲取的。
禁止重排序
volatile 禁止了指令重排。 比 sychronized 更輕量級的同步鎖
在訪問 volatile 變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此 volatile 變數是一種比 sychronized 關鍵字更輕量級的同步機制。volatile 適合這種場景:一個變數被多個執行緒共享,執行緒直接給這個變數賦值。
當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體拷貝變數到 CPU 快取中。如果電腦有多個 CPU,每個執行緒可能在不同的 CPU 上被處理,這意味著每個執行緒可以拷貝到不同的 CPU cache 中。而聲明變數是 volatile 的,JVM 保證了每次讀變數都從記憶體中讀,跳過 CPU cache 這一步。
適用場景
值得說明的是對 volatile 變數的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變數,但是並不能保證 i++這種操作的原子性,因為本質上 i++是讀、寫兩次操作。在某些場景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,只有在一些特殊的場景下,才能適用 volatile。總的來說,必須同時滿足下面兩個條件才能保證在並發環境的執行緒安全:
(1)對變數的寫操作不依賴於當前值(比如 i++),或者說是單純的變數賦值(boolean
flag = true)。
(2)該變數沒有包含在具有其他變數的不變式中,也就是說,不同的 volatile 變數之間,不能互相依賴。只有在狀態真正獨立於程式內其他內容時才能使用 volatile。
16. 如何在兩個執行緒之間共享數據
Java 裡面進行多執行緒通訊的主要方式就是共享記憶體的方式,共享記憶體主要的關注點有兩個:可見性和有序性原子性。Java 記憶體模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題,理想情況下我們希望做到「同步」和「互斥」。有以下常規實現方法:
將數據抽象成一個類,並將數據的操作作為這個類的方法
- 將數據抽象成一個類,並將對這個數據的操作作為這個類的方法,這麼設計可以和容易做到同步,只要在方法上加」synchronized「。
Runnable 對象作為一個類的內部類
- 將 Runnable 對象作為一個類的內部類,共享數據作為這個類的成員變數,每個執行緒對共享數據的操作方法也封裝在外部類,以便實現對數據的各個操作的同步和互斥,作為內部類的各個 Runnable 對象調用外部類的這些方法。
17. ThreadLocal 作用(執行緒本地存儲)
ThreadLocal,很多地方叫做執行緒本地變數,也有些地方叫做執行緒本地存儲,ThreadLocal 的作用是提供執行緒內的局部變數,這種變數在執行緒的生命周期內起作用,減少同一個執行緒內多個函數或者組件之間一些公共變數的傳遞的複雜度。
ThreadLocalMap(執行緒的一個屬性)
1. 每個執行緒中都有一個自己的 ThreadLocalMap 類對象,可以將執行緒自己的對象保持到其中,
各管各的,執行緒可以正確的訪問到自己的對象。
2. 將一個共用的 ThreadLocal 靜態實例作為 key,將不同對象的引用保存到不同執行緒的
ThreadLocalMap 中,然後在執行緒執行的各處通過這個靜態 ThreadLocal 實例的 get()方法取
得自己執行緒保存的那個對象,避免了將這個對象作為參數傳遞的麻煩。
3. ThreadLocalMap 其實就是執行緒裡面的一個屬性,它在 Thread 類中定義
ThreadLocal.ThreadLocalMap threadLocals = null;
使用場景
最常見的 ThreadLocal 使用場景為 用來解決 資料庫連接、Session 管理等。

18. synchronized 和 ReentrantLock 的區別
兩者的共同點:
1. 都是用來協調多執行緒對共享對象、變數的訪問
2. 都是可重入鎖,同一執行緒可以多次獲得同一個鎖
3. 都保證了可見性和互斥性
兩者的不同點:
1. ReentrantLock 顯示的獲得、釋放鎖,synchronized 隱式獲得釋放鎖
2. ReentrantLock 可響應中斷、可輪迴,synchronized 是不可以響應中斷的,為處理鎖的
不可用性提供了更高的靈活性
3. ReentrantLock 是 API 級別的,synchronized 是 JVM 級別的
4. ReentrantLock 可以實現公平鎖
5. ReentrantLock 通過 Condition 可以綁定多個條件
6. 底層實現不一樣, synchronized 是同步阻塞,使用的是悲觀並發策略,lock 是同步非阻
塞,採用的是樂觀並發策略
7. Lock 是一個介面,而 synchronized 是 Java 中的關鍵字,synchronized 是內置的語言
實現。
8. synchronized 在發生異常時,會自動釋放執行緒佔有的鎖,因此不會導致死鎖現象發生;
而 Lock 在發生異常時,如果沒有主動通過 unLock()去釋放鎖,則很可能造成死鎖現象,
因此使用 Lock 時需要在 finally 塊中釋放鎖。
9. Lock 可以讓等待鎖的執行緒響應中斷,而 synchronized 卻不行,使用 synchronized 時,
等待的執行緒會一直等待下去,不能夠響應中斷。
10. 通過 Lock 可以知道有沒有成功獲取鎖,而 synchronized 卻無法辦到。
11. Lock 可以提高多個執行緒進行讀操作的效率,既就是實現讀寫鎖等。
19. Java 中用到的執行緒調度
搶佔式調度
搶佔式調度指的是每條執行緒執行的時間、執行緒的切換都由系統控制,系統控制指的是在系統某種運行機制下,可能每條執行緒都分同樣的執行時間片,也可能是某些執行緒執行的時間片較長,甚至某些執行緒得不到執行的時間片。在這種機制下,一個執行緒的堵塞不會導致整個進程堵塞。
協同式調度
協同式調度指某一執行緒執行完後主動通知系統切換到另一執行緒上執行,這種模式就像接力賽一樣,一個人跑完自己的路程就把接力棒交接給下一個人,下個人繼續往下跑。執行緒的執行時間由執行緒本身控制,執行緒切換可以預知,不存在多執行緒同步問題,但它有一個致命弱點:如果一個執行緒編寫有問題,運行到一半就一直堵塞,那麼可能導致整個系統崩潰。
JVM 的執行緒調度實現(搶佔式調度)
java 使用的執行緒調使用搶佔式調度,Java 中執行緒會按優先順序分配 CPU 時間片運行,且優先順序越高越優先執行,但優先順序高並不代表能獨自佔用執行時間片,可能是優先順序高得到越多的執行時間片,反之,優先順序低的分到的執行時間少但不會分配不到執行時間。
執行緒讓出 cpu 的情況
1. 當前運行執行緒主動放棄 CPU,JVM 暫時放棄 CPU 操作(基於時間片輪轉調度的 JVM 操作系
統不會讓執行緒永久放棄 CPU,或者說放棄本次時間片的執行權),例如調用 yield()方法。
2. 當前運行執行緒因為某些原因進入阻塞狀態,例如阻塞在 I/O 上。
3. 當前運行執行緒結束,即運行完 run()方法裡面的任務。
20. 什麼是 CAS(比較並交換-樂觀鎖機制-鎖自旋)
概念及特性
CAS(Compare And Swap/Set)比較並交換,CAS 演算法的過程是這樣:它包含 3 個參數
CAS(V,E,N)。V 表示要更新的變數(記憶體值),E 表示預期值(舊的),N 表示新值。當且僅當 V 值等於 E 值時,才會將 V 的值設為 N,如果 V 值和 E 值不同,則說明已經有其他執行緒做了更新,則當前執行緒什麼都不做。最後,CAS 返回當前 V 的真實值。
CAS 操作是抱著樂觀的態度進行的(樂觀鎖),它總是認為自己可以成功完成操作。當多個執行緒同時使用 CAS 操作一個變數時,只有一個會勝出,並成功更新,其餘均會失敗。失敗的執行緒不會被掛起,僅是被告知失敗,並且允許再次嘗試,當然也允許失敗的執行緒放棄操作。基於這樣的原理,CAS 操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾,並進行恰當的處理。
原子包 java.util.concurrent.atomic(鎖自旋)
JDK1.5 的原子包:java.util.concurrent.atomic 這個包裡面提供了一組原子類。其基本的特性就是在多執行緒環境下,當有多個執行緒同時執行這些類的實例包含的方法時,具有排他性,即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由 JVM 從等待隊列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解。
相對於對於 synchronized 這種阻塞演算法,CAS 是非阻塞演算法的一種常見實現。由於一般 CPU 切換時間比 CPU 指令集操作更加長, 所以 J.U.C 在性能上有了很大的提升。如下程式碼。

getAndIncrement 採用了 CAS 操作,每次從記憶體中讀取數據然後將此數據和+1 後的結果進行CAS 操作,如果成功就返回結果,否則重試直到成功為止。而 compareAndSet 利用 JNI 來完成CPU 指令的操作。
ABA 問題
CAS 會導致「ABA 問題」。CAS 演算法實現一個重要前提需要取出記憶體中某時刻的數據,而在下時刻比較並替換,那麼在這個時間差類會導致數據的變化。
比如說一個執行緒 one 從記憶體位置 V 中取出 A,這時候另一個執行緒 two 也從記憶體中取出 A,並且two 進行了一些操作變成了 B,然後 two 又將 V 位置的數據變成 A,這時候執行緒 one 進行 CAS 操作發現記憶體中仍然是 A,然後 one 操作成功。儘管執行緒 one 的 CAS 操作成功,但是不代表這個過程就是沒有問題的。
部分樂觀鎖的實現是通過版本號(version)的方式來解決 ABA 問題,樂觀鎖每次在執行數據的修改操作時,都會帶上一個版本號,一旦版本號和數據的版本號一致就可以執行修改操作並對版本號執行+1 操作,否則就執行失敗。因為每次操作的版本號都會隨之增加,所以不會出現 ABA 問題,因為版本號只會增加不會減少。
21. 什麼是 AQS(抽象的隊列同步器)
AbstractQueuedSynchronizer 類如其名,抽象的隊列式的同步器,AQS 定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock/Semaphore/CountDownLatch。
它維護了一個 volatile int state(代表共享資源)和一個 FIFO 執行緒等待隊列(多執行緒爭用資源被
阻塞時會進入此隊列)。這裡 volatile 是核心關鍵詞,具體 volatile 的語義,在此不述。state 的
訪問方式有三種:
getState()
setState()
compareAndSetState()
AQS 定義兩種資源共享方式
xclusive 獨佔資源-ReentrantLock
Exclusive(獨佔,只有一個執行緒能執行,如 ReentrantLock)
Share 共享資源-Semaphore/CountDownLatch
Share(共享,多個執行緒可同時執行,如 Semaphore/CountDownLatch)。
AQS 只是一個框架,具體資源的獲取/釋放方式交由自定義同步器去實現,AQS 這裡只定義了一個介面,具體資源的獲取交由自定義同步器去實現了(通過 state 的 get/set/CAS)之所以沒有定義成abstract ,是因為獨佔模式下只用實現 tryAcquire-tryRelease ,而共享模式下只用實現tryAcquireShared-tryReleaseShared。如果都定義成abstract,那麼每個模式也要去實現另一模式下的介面。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放方式即可,至於具體執行緒等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS 已經在頂層實現好了。自定義同步器實現時主要實現以下幾種方法:
1.isHeldExclusively():該執行緒是否正在獨佔資源。只有用到 condition 才需要去實現它。
2.tryAcquire(int):獨佔方式。嘗試獲取資源,成功則返回 true,失敗則返回 false。 3.tryRelease(int):獨佔方式。嘗試釋放資源,成功則返回 true,失敗則返回 false。 4.tryAcquireShared(int):共享方式。嘗試獲取資源。負數表示失敗;0 表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
5.tryReleaseShared(int):共享方式。嘗試釋放資源,如果釋放後允許喚醒後續等待結點返回true,否則返回 false。
同步器的實現是 ABS 核心(state 資源狀態計數)
同步器的實現是 ABS 核心,以 ReentrantLock 為例,state 初始化為 0,表示未鎖定狀態。A 執行緒lock()時,會調用 tryAcquire()獨佔該鎖並將 state+1。此後,其他執行緒再 tryAcquire()時就會失敗,直到 A 執行緒 unlock()到 state=0(即釋放鎖)為止,其它執行緒才有機會獲取該鎖。當然,釋放鎖之前,A 執行緒自己是可以重複獲取此鎖的(state 會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證 state 是能回到零態的。
以 CountDownLatch 以例,任務分為 N 個子執行緒去執行,state 也初始化為 N(注意 N 要與執行緒個數一致)。這 N 個子執行緒是並行執行的,每個子執行緒執行完後 countDown()一次,state會 CAS 減 1。等到所有子執行緒都執行完後(即 state=0),會 unpark()主調用執行緒,然後主調用執行緒就會從 await()函數返回,繼續後余動作。
ReentrantReadWriteLock 實現獨佔和共享兩種方式
一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一種即可。但 AQS 也支援自定義同步器同時實現獨佔和共享兩種方式,如 ReentrantReadWriteLock。

