標籤:可見度 path unpark static dac syn 維護 可見 thread
我們說的AQS就是AbstractQueuedSynchronizer
,他在java.util.concurrent.locks
包下,這個類是Java並發的一個核心類。第一次知道有這個類是在看可重新進入鎖ReentrantLock
中,在ReentrantLock
中有一個內部類Sync
繼承於AbstractQueuedSynchronizer
,是ReentrantLock
的核心實現。在並發包中的鎖幾乎都是基於AQS來構建的,但是在看源碼的時候就會發現他們並沒有直接繼承AbstractQueuedSynchronizer
,而是通過內部類Sync
實現。
abstract static class Sync extends AbstractQueuedSynchronizer
這裡注意的是AbstractQueuedSynchronizer
是一個抽象類別,定義了基本的架構。AQS核心是用一個變數state
來表示狀態.
AQS也就是AbstractQueuedSynchronizer
這個類只是定義了一個隊列管理線程,對於線程的狀態是子類維護的,我們可以理解為師一個同步隊列,當有線程擷取鎖失敗時(多線程爭用資源被阻塞時會進入此隊列),線程會被添加到隊列的隊尾
總結:
- AQS只是負責管理線程阻塞隊列。
- 線程的阻塞和喚醒
同步器是實現鎖的關鍵(例如AQS隊列同步器),利用同步器實現鎖的定義。鎖匙面向使用者的,它定義了使用者和鎖互動的介面,但是隱藏了實現的細節。同步器則是鎖的實現,所以他是在鎖的背後默默做著貢獻,使用者不能直接的接觸到他,他簡化了鎖的實現方式,屏蔽了同步狀態管理、線程之間的排隊、等待、喚醒等操作。這樣設計很好的隔離了使用者和實現者關注的領域。
上面的表示了隊列的形態,head
表示隊列的前端節點,tail
表示隊列的尾節點。在源碼中他們的定義使用volatile
定義的。使用volatile
關鍵字保證了變數在記憶體中的可見度,詳見:volatile關鍵字解析。保證某個線程在出隊入隊時被其他線程看到。
private transient volatile Node head;//前端節點private transient volatile Node tail;//尾節點
在AbstractQueuedSynchronizer
這個類中還有一個內部類Node
,用於構建隊列元素的節點類。
在AQS中定義了兩種資源共用方式:
- Exclusive:獨佔式
Share:共用式
當以獨佔模式擷取時,嘗試通過其他線程擷取不能成功。 多線程擷取的共用模式可能(但不需要)成功。 當共用模式擷取成功時,下一個等待線程(如果存在)也必須確定它是否也可以擷取。 在不同模式下等待的線程共用相同的FIFO隊列。
在不同的實作類別中為了實現不同的功能,會採用不同的共用方式,例如可重新進入鎖ReentrantLock
採用的就是獨佔鎖。
AQS的不同實作類別,不需要關注線程等待隊列的維護和管理(線程阻塞入隊、喚醒出隊),在AQS中這些是已經定義好的,不同的同步器只需要對以下方法進行實現即可:
//獨佔方式嘗試擷取資源protected boolean tryAcquire(int arg)//獨佔方式嘗試釋放資源protected boolean tryRelease(int arg)//共用方式嘗試擷取資源,傳回值0表示成功但是沒有剩餘資源,負數表示失敗,正數表示成功且有剩餘資源protected int tryAcquireShared(int arg)//共用方式嘗試釋放資源protected boolean tryReleaseShared(int arg)
所有自訂的同步器只需要確定自己是那種資源貢獻方式即可:共用式、獨佔式。也可以同時實現共用式和獨佔式ReentrantReadWriteLock
讀寫鎖,多個線程可以同時進行讀操作,但是只能有一個線程進行寫操作。
獨佔模式同步狀態擷取:
首先先從代碼開始執行的地方看:
以獨佔模式擷取資源,忽略中斷。(如果擷取到資源,直接返回結果,否則進入等待隊列,等待再次擷取資源。) 通過調用至少一次tryAcquire(int)
實現,成功返回。 否則線程排隊,可能會重複阻塞和解除阻塞,直到成功才調用tryAcquire(int)
。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
方法執行的順序:
- 調用
tryAcquire()
方法嘗試去擷取資源,具體在子類中進行實現
- 調用
addWaiter()
方法把當前線程標記為獨佔式,並加入到隊列的尾部
這裡需要講一下addWaiter()
方法中的第一個參數,線程等待隊列中的元素都是利用Node
這個內部類儲存的,在Node
中有兩個成員變數分別聲明了資源共用方式:
static final Node SHARED = new Node();//共用式 static final Node EXCLUSIVE = null;//獨佔式
- 調用
acquireQueued()
方法,讓線程在隊列中等待擷取資源,擷取資源後返回,如果在這個等待過程中線程被中斷過,返回true,否則返回false
在方法中首先調用tryAcquire(int)
方法,該方法在AbstractQueuedSynchronizer
並沒有實現,需要子類去實現:
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }
第二步調用addWaiter()
方法:該方法是負責維護線程等待隊列的方法,所以在AbstractQueuedSynchronizer
中實現了該方法:具體是建立了一個節點類,把節點放在隊尾,如果失敗調用enq(node)
方法(隊尾節點為空白)。
addWaiter()方法:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
上面的方法判斷,如果添加到隊尾失敗
enq()方法:
private Node enq(final Node node) { for (;;) { Node t = tail; //如果隊列為空白(隊尾元素為空白)建立節點添加進去 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) //把tail指向head tail = head; } else { //正常添加到隊尾 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
在上面的代碼中添加節點都用到了比較和交換(CAS,可以說是一種在並發環境下的解決方案),compareAndSetTail()
方法能夠確保節點能被安全的添加進隊列中,在多線程環境下無法保證一個元素被正確的添加到隊列的尾部。因為進入隊列的元素都是放在隊尾的,為了保證資料的正確性,所以在設定尾節點的時候使用CAS。
第三步調用acquireQueued()
方法,目的是為了在隊列中等待被喚醒使用資源,因為之前的操作失敗後,線程會被放入隊尾,隊列是先進先出的結構,所以在隊尾的線程必須等待被喚醒。方法中主要有一個死迴圈,我們稱他叫自旋,只有當條件滿足的時候,獲得同步狀態,退出自旋。
acquireQueued()方法:
final boolean acquireQueued(final Node node, int arg) { //設定成功標記 boolean failed = true; try { //設定中斷標記 boolean interrupted = false; for (;;) { //獲得node的前驅節點 final Node p = node.predecessor(); //判斷前驅結點是否是前端節點 if (p == head && tryAcquire(arg)) { //把node設定為頭結點 setHead(node); //把p節點的前驅設定為null,見下面的解釋 p.next = null; // help GC failed = false; return interrupted; } //判斷是否繼續等待 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
把p節點的前驅設定為null,也就是之前的head節點,在上面源碼中後面的注釋標記為help GC功能,解釋一下:在調用上面的setHead()方法的時候,方法的內部已經將當前節點的前驅結點設定為null,在這裡再次設定一遍,為了保證當前節點的前驅結點順利被回收(當前節點設定為前端節點,那麼之前的前端節點就要被釋放,類比一個正常的出隊過程)。自己畫圖更好理解。
setHead()方法:
private void setHead(Node node) { head = node; node.thread = null; node.prev = null; }
這裡分析上面調用的acquireQueued()
方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //擷取前驅節點的狀態 int ws = pred.waitStatus; //如果當前節點狀態值為SIGNAL這個值,代表當前線程應該被掛起,等待被喚醒 if (ws == Node.SIGNAL) return true; if (ws > 0) { //如果大於0代表將當前節點的前驅節點移除 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //小於0時把前驅結點狀態值設定為SIGNAL,目的是為了前驅判斷後將當前節點掛起(通知自己一下) compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
在這裡我們需要看一下Node
這個類中定義的關於狀態值的定義:
//表示線程已取消,作廢狀態 static final int CANCELLED = 1; //表示後繼節點應該等待當前節點釋放資源後喚醒其後繼節點 static final int SIGNAL = -1; //表示當前正處於等待狀態 static final int CONDITION = -2; //表示狀態需要向後傳播 static final int PROPAGATE = -3;
- CANCELLED 取消狀態
- SIGNAL 等待觸發狀態
- CONDITION 等待條件狀態
- PROPAGATE 狀態需要向後傳播
等待隊列是FIFO先進先出,只有前一個節點的狀態為SIGNAL時,當前節點的線程才能被掛起。 所以在方法調用的時候把前驅結點設定為SIGNAL。
因為前一節點被置為SIGNAL說明後面有線程需要執行,但是還輪不到它後面的線程執行,後麵線程一定要找一個前驅節點不為CANCEL的節點,然後把它設定為SIGNAL然後原地掛起,等待喚醒。 因為SIGNAL執行完了會喚醒緊接著的後面一個。
總結:
AQS中定義的acquire()
模板方法,具體通過調用子類中的tryAcquire()
方法嘗試去擷取資源,成功則返回,失敗調用addWaiter()
將當前線程添加到阻塞隊列的隊尾,同時標記為獨佔狀態。acquireQueued()
方法通過自旋擷取同步狀態(該方法使線程在等待隊列中等待休息,當有機會時嘗試擷取資源),節點嘗試擷取資源的條件是當前節點的前驅節點是前端節點,嘗試擷取到資源後才返回,在整個等待過程中如果發生過中斷,不做響應,在擷取資源後調用selfInterrupt()
方法設定中斷。
獨佔模式下同步狀態的釋放:
上面根據源碼分析了獨佔模式下獲得鎖的過程主要調用了模板方法acquire()
方法向下分析,接著我們分析它的相反的方法,獨佔模式下釋放鎖的過程,還是一個模板方法release()
public final boolean release(int arg) { if (tryRelease(arg)) { //得到前端節點 Node h = head; //判斷前端節點不為空白,狀態值符合條件 if (h != null && h.waitStatus != 0) //喚醒下一個等待線程 unparkSuccessor(h); return true; } return false; }
tryRelease()
方法依然需要子類去自己實現
protected boolean tryRelease(int arg) { throw new UnsupportedOperationException(); }
unparkSuccessor()
方法:
private void unparkSuccessor(Node node) { //獲得當前線程的狀態值 int ws = node.waitStatus; if (ws < 0) //小於0時置零 compareAndSetWaitStatus(node, ws, 0); //獲得當前節點的後繼節點 Node s = node.next; //判斷為空白和狀態值是否大於0 if (s == null || s.waitStatus > 0) { s = null; //從尾節點向前遍曆,需要喚醒的線程通常是儲存在下一個節點中的 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //喚醒線程 LockSupport.unpark(s.thread); }
unpark()
方法喚醒的是等待隊列中最前面的線程,之後會再次執行上面的過程。
總結:在擷取同步狀時,在使用者的角度看在使用鎖時,同步器會維護一個同步隊列,擷取狀態失敗的線程會被加入這個隊列並進行自旋;當該節點的前驅節點是前端節點的時候並且獲得了同步狀態時移出隊列。在釋放的時候,調用tryRelease()
釋放並喚醒後繼節點。
Java並發AQS原理分析(一)