一行一行源碼分析清楚AbstractQueuedSynchronizer
轉自https://www.javadoop.com/post/AbstractQueuedSynchronizer#toc4
在分析 Java 併發包 java.util.concurrent 源碼的時候,少不了需要了解 AbstractQueuedSynchronizer(以下簡寫AQS)這個抽象類,因爲它是 Java 併發包的基礎工具類,是實現 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等類的基礎。
Google 一下 AbstractQueuedSynchronizer,我們可以找到很多關於 AQS 的介紹,但是很多都沒有介紹清楚,因爲大部分文章沒有把其中的一些關鍵的細節說清楚。
本文將從 ReentrantLock 的公平鎖源碼出發,分析下 AbstractQueuedSynchronizer 這個類是怎麼工作的,希望能給大家提供一些簡單的幫助。
申明以下幾點:
本文有點長,但是很簡單很簡單很簡單,主要面向讀者對象爲併發編程的初學者,或者想要閱讀java併發包源碼的開發者。
建議在電腦上閱讀,如果你想好好地理解所有的細節,而且你從來沒看過相關的分析,你可能至少需要 20 分鐘仔細看所有的描述,本文後面的 1/3 以上很簡單,前面的 1/4 更簡單,中間的部分要好好看。
如果你不知道爲什麼要看這個,我想告訴你,即使你看懂了所有的細節,你可能也不能把你的業務代碼寫得更好
源碼環境 JDK1.7,看到不懂或有疑惑的部分,最好能自己打開源碼看看。Doug Lea 大神的代碼寫得真心不錯。
有很多英文註釋我沒有刪除,這樣讀者可以參考着英文說的來,萬一被我忽悠了呢
本文不分析共享模式,這樣可以給讀者減少很多負擔,只要把獨佔模式看懂,共享模式讀者應該就可以順着代碼看懂了。而且也不分析 condition 部分,所以應該說很容易就可以看懂了。
本文大量使用我們平時用得最多的 ReentrantLock 的概念,本質上來說是不正確的,讀者應該清楚,AQS 不僅僅用來實現鎖,只是希望讀者可以用鎖來聯想 AQS 的使用場景,降低讀者的閱讀壓力
ReentrantLock 的公平鎖和非公平鎖只有一點點區別,沒有任何閱讀壓力
你需要提前知道什麼是 CAS(CompareAndSet)
廢話結束,開始。
CLH隊列
此篇博客所有源碼均來自JDK 1.8
AQS內部維護着一個FIFO隊列,該隊列就是CLH同步隊列。
CLH同步隊列是一個FIFO雙向隊列,AQS依賴它來完成同步狀態的管理,當前線程如果獲取同步狀態失敗時,AQS則會將當前線程已經等待狀態等信息構造成一個節點(Node)並將其加入到CLH同步隊列,同時會阻塞當前線程,當同步狀態釋放時,會把首節點喚醒(公平鎖),使其再次嘗試獲取同步狀態。
在CLH同步隊列中,一個節點表示一個線程,它保存着線程的引用(thread)、狀態(waitStatus)、前驅節點(prev)、後繼節點(next),其定義如下:
static final class Node { /** 共享 */ static final Node SHARED = new Node(); /** 獨佔 */ static final Node EXCLUSIVE = null; /** * 因爲超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到競爭中的,他會一直保持取消狀態不會轉變爲其他狀態; */ static final int CANCELLED = 1; /** * 後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行 */ static final int SIGNAL = -1; /** * 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中 */ static final int CONDITION = -2; /** * 表示下一次共享式同步狀態獲取將會無條件地傳播下去 */ static final int PROPAGATE = -3; /** 等待狀態 */ volatile int waitStatus; /** 前驅節點 */ volatile Node prev; /** 後繼節點 */ volatile Node next; /** 獲取同步狀態的線程 */ volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus; this.thread = thread; }} final class Node { /** 共享 */ static final Node SHARED = new Node(); /** 獨佔 */ static final Node EXCLUSIVE = null; /** * 因爲超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到競爭中的,他會一直保持取消狀態不會轉變爲其他狀態; */ static final int CANCELLED = 1; /** * 後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行 */ static final int SIGNAL = -1; /** * 節點在等待隊列中,節點線程等待在Condition上,當其他線程對Condition調用了signal()後,改節點將會從等待隊列中轉移到同步隊列中,加入到同步狀態的獲取中 */ static final int CONDITION = -2; /** * 表示下一次共享式同步狀態獲取將會無條件地傳播下去 */ static final int PROPAGATE = -3; /** 等待狀態 */ volatile int waitStatus; /** 前驅節點 */ volatile Node prev; /** 後繼節點 */ volatile Node next; /** 獲取同步狀態的線程 */ volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus; this.thread = thread; }}
CLH同步隊列結構圖如下:
入列
學了數據結構的我們,CLH隊列入列是再簡單不過了,無非就是tail指向新節點、新節點的prev指向當前最後的節點,當前最後一個節點的next指向當前節點。代碼我們可以看看addWaiter(Node node)方法:
private Node addWaiter(Node mode) { //新建Node Node node = new Node(Thread.currentThread(), mode); //快速嘗試添加尾節點 Node pred = tail; if (pred != null) { node.prev = pred; //CAS設置尾節點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //多次嘗試 enq(node); return node; }private Node addWaiter(Node mode) { //新建Node Node node = new Node(Thread.currentThread(), mode); //快速嘗試添加尾節點 Node pred = tail; if (pred != null) { node.prev = pred; //CAS設置尾節點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //多次嘗試 enq(node); return node; }
addWaiter(Node node)先通過快速嘗試設置尾節點,如果失敗,則調用enq(Node node)方法設置尾節點
private Node enq(final Node node) { //多次嘗試,直到成功爲止 for (;;) { Node t = tail; //tail不存在,設置爲首節點 if (t == null) { if (compareAndSetHead(new Node())) tail = head; } else { //設置爲尾節點 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }private Node enq(final Node node) { //多次嘗試,直到成功爲止 for (;;) { Node t = tail; //tail不存在,設置爲首節點 if (t == null) { if (compareAndSetHead(new Node())) tail = head; } else { //設置爲尾節點 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
在上面代碼中,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法可以確保節點是線程安全添加的。在enq(Node node)方法中,AQS通過“死循環”的方式來保證節點可以正確添加,只有成功添加後,當前線程纔會從該方法返回,否則會一直執行下去。
過程圖如下:
出列
CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,注意在這個過程是不需要使用CAS來保證的,因爲只有一個線程能夠成功獲取到同步狀態。過程圖如下:
AQS 結構
先來看看 AQS 有哪些屬性,搞清楚這些基本就知道 AQS 是什麼套路了,畢竟可以猜嘛!
// 頭結點,你直接把它當做 當前持有鎖的線程 可能是最好理解的private transient volatile Node head;// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個隱視的鏈表private transient volatile Node tail;// 這個是最重要的,不過也是最簡單的,代表當前鎖的狀態,0代表沒有被佔用,大於0代表有線程持有當前鎖// 之所以說大於0,而不是等於1,是因爲鎖可以重入嘛,每次重入都加上1private volatile int state;// 代表當前持有獨佔鎖的線程,舉個最重要的使用例子,因爲鎖可以重入// reentrantLock.lock()可以嵌套調用多次,所以每次用這個來判斷當前線程是否已經擁有了鎖// if (currentThread == getExclusiveOwnerThread()) {state++}private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer
怎麼樣,看樣子應該是很簡單的吧,畢竟也就四個屬性啊。
AbstractQueuedSynchronizer 的等待隊列示意如下所示,注意了,之後分析過程中所說的 queue,也就是阻塞隊列不包含 head,不包含 head,不包含 head。
等待隊列中每個線程被包裝成一個 node,數據結構是鏈表,一起看看源碼吧:
static final class Node { /** Marker to indicate a node is waiting in shared mode */ // 標識節點當前在共享模式下 static final Node SHARED = new Node(); /** Marker to indicate a node is waiting in exclusive mode */ // 標識節點當前在獨佔模式下 static final Node EXCLUSIVE = null; // ======== 下面的幾個int常量是給waitStatus用的 =========== /** waitStatus value to indicate thread has cancelled */ // 代碼此線程取消了爭搶這個鎖 static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ // 官方的描述是,其表示當前node的後繼節點對應的線程需要被喚醒 static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ // 本文不分析condition,所以略過吧,下一篇文章會介紹這個 static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ // 同樣的不分析,略過吧 static final int PROPAGATE = -3; // ===================================================== // 取值爲上面的1、-1、-2、-3,或者0(以後會講到) // 這麼理解,暫時只需要知道如果這個值 大於0 代表此線程取消了等待, // 也許就是說半天搶不到鎖,不搶了,ReentrantLock是可以指定timeouot的。。。 volatile int waitStatus; // 前驅節點的引用 volatile Node prev; // 後繼節點的引用 volatile Node next; // 這個就是線程本尊 volatile Thread thread; }
Node 的數據結構其實也挺簡單的,就是 thread + waitStatus + pre + next 四個屬性而已,大家先要有這個概念在心裏。
上面的是基礎知識,後面會多次用到,心裏要時刻記着它們,心裏想着這個結構圖就可以了。下面,我們開始說 ReentrantLock 的公平鎖。多嘴一下,我說的阻塞隊列不包含 head 節點。
首先,我們先看下 ReentrantLock 的使用方式。
// 我用個web開發中的service概念吧public class OrderService { // 使用static,這樣每個線程拿到的是同一把鎖,當然,spring mvc中service默認就是單例,別糾結這個 private static ReentrantLock reentrantLock = new ReentrantLock(true); public void createOrder() { // 比如我們同一時間,只允許一個線程創建訂單 reentrantLock.lock(); // 通常,lock 之後緊跟着 try 語句 try { // 這塊代碼同一時間只能有一個線程進來(獲取到鎖的線程), // 其他的線程在lock()方法上阻塞,等待獲取到鎖,再進來 // 執行代碼... // 執行代碼... // 執行代碼... } finally { // 釋放鎖 reentrantLock.unlock(); } } }
ReentrantLock 在內部用了內部類 Sync 來管理鎖,所以真正的獲取鎖和釋放鎖是由 Sync 的實現類來控制的。
abstract static class Sync extends AbstractQueuedSynchronizer { }
Sync 有兩個實現,分別爲 NonfairSync(非公平鎖)和 FairSync(公平鎖),我們看 FairSync 部分。
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
線程搶鎖
很多人肯定開始嫌棄上面廢話太多了,下面跟着代碼走,我就不廢話了。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; // 爭鎖 final void lock() { acquire(1); } // 來自父類AQS,我直接貼過來這邊,下面分析的時候同樣會這樣做,不會給讀者帶來閱讀壓力 // 我們看到,這個方法,如果tryAcquire(arg) 返回true, 也就結束了。 // 否則,acquireQueued方法會將線程壓到隊列中 public final void acquire(int arg) { // 此時 arg == 1 // 首先調用tryAcquire(1)一下,名字上就知道,這個只是試一試 // 因爲有可能直接就成功了呢,也就不需要進隊列排隊了, // 對於公平鎖的語義就是:本來就沒人持有鎖,根本沒必要進隊列等待(又是掛起,又是等待被喚醒的) if (!tryAcquire(arg) && // tryAcquire(arg)沒有成功,這個時候需要把當前線程掛起,放到阻塞隊列中。 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); } } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ // 嘗試直接獲取鎖,返回值是boolean,代表是否獲取到鎖 // 返回true:1.沒有線程在等待鎖;2.重入鎖,線程本來就持有鎖,也就可以理所當然可以直接獲取 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // state == 0 此時此刻沒有線程持有鎖 if (c == 0) { // 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來後到, // 看看有沒有別人在隊列中等了半天了 if (!hasQueuedPredecessors() && // 如果沒有線程在等待,那就用CAS嘗試一下,成功了就獲取到鎖了, // 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個線程搶先了 =_= // 因爲剛剛還沒人的,我判斷過了