深刻理解Java併發框架AQS系列(一):線程
深刻理解Java併發框架AQS系列(二):AQS框架簡介及鎖概念
深刻理解Java併發框架AQS系列(三):獨佔鎖(Exclusive Lock)html
優秀的源碼就在那裏java
通過了前面兩章的鋪墊,終於要切入正題了,本章也是整個AQS的核心之一api
從本章開始,咱們要精讀AQS源碼,在欣賞它的同時也要學會質疑它。固然本文不會帶着你們逐行過源碼(會有「只在此山中,雲深不知處」的弊端),而是從功能入手,對其架構進行逐層剖析,在覈心位置重點解讀,並提出質疑;雖然AQS源碼讀起來比較「跳」,但我仍是建議你們花時間及精力去好好讀它數據結構
本章咱們採用經典併發類ReentrantLock
來闡述獨佔鎖多線程
獨佔鎖,顧名思義,即在同一時刻,僅容許一個線程執行同步塊代碼。比如一夥兒人想要過河,但只有一根獨木橋,且只能承受一人的重量架構
相信咱們平時寫獨佔鎖的程序大抵是這樣的:併發
ReentrantLock lock = new ReentrantLock(); try { lock.lock(); doBusiness(); } finally { lock.unlock(); }
上述代碼分爲三部分:框架
lock.lock()
doBusiness()
lock.unlock()
加鎖部分,必定是衆矢之的,兵家爭搶的要地,對於高併發的程序來講,同一時刻,大量的線程爭相涌入,而lock()
則保證只能有一個線程進入doBusiness()
邏輯,且在其執行完畢unlock()
方法以前,不能有其餘線程進入。因此相對而言,unlock()
方法相對輕鬆,不用處理多線程的場景ide
waitStatus
本章中,咱們引入節點中一個關鍵的字段waitStatus
(後文簡寫爲ws
),在獨佔鎖模式中,可能會使用到的等待狀態以下:高併發
0
ws
爲0SIGNAL (-1)
SIGNAL
,即代表其後續節點處於(或即將處於)阻塞狀態。因此當前節點在執行完同步代碼或被取消後,必定要記得喚醒其後續節點CANCELLED (1)
tryAcquire
發生異常,都會致使當前節點取消。而當節點一旦取消,便永遠不會再變爲0
或者SIGNAL
狀態了咱們先上一張ReentrantLock
加鎖功能(非公平)的總體流程圖,在併發或關鍵部分有註釋
第一眼看上去,確實有點複雜,不過不用怕,咱們逐一分析解讀後,它其實就是隻紙老虎
大致上能夠分爲三大部分
按照正常的理解,可能只會有a、b兩部分就夠了,爲何會有c呢?何時會發生異常?
當一個線程嘗試加鎖失敗後,便會放入阻塞隊列的隊尾;這節咱們來討論一下這個動做的細節
在加入阻塞隊列以前,首先會查看頭節點是否爲null,若是是null的話,須要新建ws
爲0的頭結點,(爲何在AQS初始化的時候,不直接新建頭結點呢?其實因而可知做者細節處理的嚴謹,由於若是當咱們的獨佔鎖併發度不大,在嘗試加鎖的過程當中,總能獲取到鎖,這時便不會向阻塞隊列添加內容,假如初始化便新建頭結點,會致使其白白佔用內存空間而得不到有效利用)而後將當前節點添加至阻塞隊列的尾部,固然頭結點初始化、向尾部節點追加新節點都是經過CAS操做的。而阻塞隊列呢,正如咱們前文說起的是一個FIFO的隊列,且帶有next
、prev
兩個引用來標記前、後節點;咱們在阻塞隊列中加入第一個節點後,阻塞隊列的樣子:
這一節屬於獨佔鎖很核心的部分,裏面涉及ws
更改、線程掛起與喚醒、更換頭結點等
咱們接着3.1繼續,在節點進入調度後,首先檢查下當前節點的前節點是否爲head
節點,若是是的話,那麼有一次嘗試加鎖的機會,加鎖成功或失敗將致使2個分支
咱們首先看加鎖加鎖成功的狀況,一旦加鎖成功,當前節點便從阻塞隊列中「消失」(實際上是當前節點變爲了頭結點,而原頭結點內存不可達,等待垃圾回收),當全部節點都加鎖成功,阻塞隊列便爲空了,但並不表明阻塞隊列的長度爲0,由於有頭結點的存在,因此空阻塞隊列的長度是1
而加鎖失敗或者當前節點的前節點不是head
節點呢?是立刻將線程掛起嗎?答案是不肯定的,要看前節點的ws
狀態而定。而此步驟還有個隱藏任務:將當前節點以前的全部已取消節點從阻塞隊列中剔除。
從上圖中咱們看到,一個節點若是想正常進入掛起狀態,那麼必定要將前節點的ws
改成SIGNAL (-1)
狀態,但若是前節點已經變爲CANCELLED (1)
狀態後,就要遞歸向前尋找第一個非CANCELLED
的節點。
針對「線程掛起並等待其餘線程喚醒」,咱們提出2個問題
問題1
問題2
park/unpark
,即使是unpark發生在park以前,在執行park操做時,也會成功喚醒。這個特質區別於wait/notify
而針對阻塞隊列的調度,還有一些沒有解釋的問題:
CANCELLED
狀態的節點?SIGNAL
狀態,但通過一段時間運行,前節點變爲了CANCELLED
狀態,豈不是致使當前節點永遠沒法被喚醒?要回答這兩個問題,就要引出異常處理了
咱們首先討論若是AQS不作異常處理能夠嗎? 不能夠,例如第一個節點被喚醒後,在加鎖階段發生了異常,若是沒有異常處理,這個異常節點將永遠處於阻塞隊列,成爲「殭屍節點」,且後續節點也不會被喚起
官方標明可能會出現異常的部分,諸如「等待超時」、「打斷」等,那若是咱們調用acquire()
方法,而非acquireInterruptibly()
、tryAcquireNanos(time)
是否是就不會出現異常?不是的,由於還有AQS下放給咱們本身實現的tryRelease()
等方法。咱們實現一個本身的AQS,並模擬tryRelease()
報錯,看AQS可否正常應對
public class FindBugAQS { public volatile static int FLAG = 0; private static ThreadLocal<Integer> FLAG_STORE = new ThreadLocal<>(); private static ThreadLocal<Integer> TIMES = ThreadLocal.withInitial(() -> 0); private Sync sync = new Sync(); private static class Sync extends AbstractQueuedSynchronizer { private Sync() { setState(1); } public void lock() { FLAG_STORE.set(++FLAG); int state = getState(); if (state == 1 && compareAndSetState(state, 0)) { return; } acquire(1); } @Override protected boolean tryAcquire(int acquires) { if (FLAG_STORE.get() == 2) { Integer time = TIMES.get(); if (time == 0) { TIMES.set(1); } else { // 模擬發生異常,第二個節點在第二次訪問tryAcquire方法時,將會扔出運行期異常 System.out.println("發生異常"); throw new RuntimeException("lkn aqs bug"); } } int state = getState(); if (state == 1 && compareAndSetState(state, 0)) { return true; } return false; } @Override protected final boolean tryRelease(int releases) { setState(1); return true; } public void unlock() { release(1); } } public void lock() { sync.lock(); } public void unlock() { sync.unlock(); } } // 測試用例以下: public class BugTest { private static volatile int number = 0; @Test public void test2() throws InterruptedException { List<Thread> list = Lists.newArrayList(); FindBugAQS aqs = new FindBugAQS(); Thread thread1 = new Thread(() -> { aqs.lock(); PubTools.sleep(5000); number++; aqs.unlock(); }); thread1.start(); list.add(thread1); PubTools.sleep(500); for (int i = 0; i < 4; i++) { Thread thread2 = new Thread(() -> { aqs.lock(); PubTools.sleep(500); number++; aqs.unlock(); }); thread2.start(); list.add(thread2); } for (Thread thread : list) { thread.join(); } System.out.println("number is " + number); } }
運行結果:
發生異常 Exception in thread "Thread-1" java.lang.RuntimeException: lkn aqs bug at org.xijiu.share.aqs.bug.FindBugAQS$Sync.tryAcquire(FindBugAQS.java:42) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:863) at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199) at org.xijiu.share.aqs.bug.FindBugAQS$Sync.lock(FindBugAQS.java:31) at org.xijiu.share.aqs.bug.FindBugAQS.lock(FindBugAQS.java:64) at org.xijiu.share.aqs.bug.BugTest.lambda$test2$2(BugTest.java:61) at java.lang.Thread.run(Thread.java:748) number is 4
咱們自定義了AQS實現類FindBugAQS.java
,模擬第二個節點在第二次訪問tryAcquire
會扔出異常;而後啓動5個線程,對number
進行累加。可見,最後的結果符合預期,AQS處理的很完美。那程序發生異常後,阻塞隊列究竟如何應對?
舉例說明吧,假定如今除去頭結點外,阻塞隊列中還有3個節點,當第1個節點被喚醒執行時,發生了異常,那麼第1個節點會將ws
置爲CANCELLED
,且將向後的鏈條打斷(指向本身),但向前鏈條保持不變,並喚醒下一個節點
由上圖可見,當某個節點響應中斷/發生異常後,其會主動打斷向後鏈條,但依舊保留向前的鏈條,這樣作的目的是爲了後續節點在尋找前節點時,能夠找到標記爲CANCELLED
狀態的節點,而不是找到null
。至此便解答了3.2提出的兩個問題
a、爲何阻塞隊列內有這麼多CANCELLED
狀態的節點?
CANCELLED
狀態,但仍存在於阻塞隊列中,直到正常執行的節點將其剔除b、當前節點在掛起前,前節點爲SIGNAL
狀態,但通過一段時間運行,前節點變爲了CANCELLED
狀態,豈不是致使當前節點永遠沒法被喚醒?
原本想針對「解鎖邏輯」畫一張流程圖,但猛然發現解鎖部分僅僅10行左右的代碼,那就索性把源碼貼上,逐一論述下
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
ReentrantLock
解鎖源碼protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
咱們發現當tryRelease()
方法返回true
時,AQS便會負責喚醒後續節點,由於ReentrantLock
支持了可重入的特性,因此當前線程的每次加鎖都會對state
累加,而每次tryRelease()
方法則會對state
累減,直到state
變爲初始狀態0時,tryRelease()
方法纔會返回true
,即喚醒下一個節點
解鎖邏輯相對簡潔,且不存在併發,本文再也不贅述
再次強調本文是經過ReentrantLock
的視角來分析獨佔鎖,且主要分析的是ReentrantLock.lock()/unlock()
方法,目的是讓你們對AQS總體的數據結構有個全面認識,方便後續在實現本身的併發框架時,明白api背後發生的事情,作到遊刃有餘
而像ReentrantLock
的lockInterruptibly()
、tryLock(TimeUnit)
或者其餘獨佔鎖的實現類,讀者可自行閱讀源碼,原理相似,核心代碼也是同樣的