隊列同步器介紹
隊列同步器AbstractQueuedSynchronizer,是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
同步器的主要使用方式是繼承,一般作為同步器組件的靜態內部類,在同步器中僅定義了與狀態相關的方法,且狀態既可以獨占獲取也可以共享獲取,這樣就可以實現不同的同步組件(ReetrantLock、CountDownLatch等)。同步組件利用同步器進行鎖的實現,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等頂層操作。 同步器的設計是基於模板方法模式的,使用者需要繼承同步器並重寫指定的方法,隨后將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法。
同步器提供了3個方法來修改和訪問同步狀態:
1、getState():獲取當前同步狀態
2、setState(int newState):設置當前同步狀態
3、compareAndSetState(int expect, int update):使用CAS設置當前狀態,該方法能保證操作的原子性。
隊列同步器的實現
下面主要從實現角度分析同步器是如何完成線程同步的,主要包括:同步隊列、獨占式同步狀態的獲取和釋放、共享式同步狀態的獲取和釋放等核心數據結構與模板方法。
1、同步隊列
同步器利用同步隊列來完成同步狀態的管理。它是一個FIFO的雙向隊列,當線程獲取狀態失敗時,同步器會將當前線程和等待狀態等信息包裝成尾節點放入同步隊列中,同時會阻塞當前線程。當同步狀態釋放時,會喚醒首節點,使其嘗試獲取同步狀態。
在節點加入過程中,會涉及到並發的問題,所以這個加入過程要確保線程安全,因此同步器提供了一個基於CAS設置尾節點的方法:compareAndSetTail(Node expect, Node update)。
在設置首節點過程中,首節點是通過獲取同步狀態成功的線程設置的,由於只有一個線程能夠獲取到同步狀態,所以設置首節點的方法並不需要使用CAS來保證。只需要將首節點設置原首節點的后續節點並斷開原節點的next引用即可。
2、獨占式同步狀態的獲取
同步器是通過acquire()方法來獲取同步狀態的,該方法是模板方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
1、調用自定義同步器實現的tryAcquire(arg)獲取同步狀態,該方法保證線程安全。
2、如果獲取同步狀態失敗,則構造同步節點(獨占式),通過addWaiter方法放入同步隊列的尾部
final boolean acquireQueued(final Node node, int arg) { try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } }
3、節點在加入同步隊列后,在同步隊列中所有的節點都處於自旋狀態,但是只有前驅節點是頭節點才能嘗試獲取同步狀態。一是因為頭節點是已經獲取到同步狀態的節點,當頭節點的線程釋放同步狀態之后,將會喚醒后繼節點,后繼節點被換喚醒后需要檢查自己的前驅節點是否是頭節點。而是因為這樣處理可以維護同步隊列的FIFO原則。
3、獨占式同步狀態的釋放
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
該方法執行時,會喚醒節點的后繼節點線程,通過調用unparkSuccessor(h)方法來喚醒處於等待狀態的線程。
4、共享式同步狀態獲取
與獨占式的區別在於:在同一時刻能否有多個線程同時獲取到同步狀態。例如文件讀寫,在同一時刻,如果進行讀操作,那么寫操作會被阻塞,但是可以同時有多個讀操作,這種讀操作就是共享式的。相反,寫操作只能有一個,並且阻塞其他所有的寫操作和讀操作。
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
private void doAcquireShared(int arg) { final Node node = addWaiter(Node.SHARED); try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } catch (RuntimeException ex) { cancelAcquire(node); throw ex; } }
1、調用tryAcquireShared(arg)嘗試獲取同步狀態,如果方法值大於等於0,則表示獲取同步狀態成功。
2、在方法doAcquireShared自旋過程中,如果前驅節點為頭節點時,嘗試獲取同步狀態。自旋結束的條件就是tryAcquireShared方法的返回值大於等於0.
5、共享式同步狀態釋放
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
這個與獨占式類似,都是釋放后將會喚醒處於等待狀態的節點。唯一的區別是這個方法必須支持並發,因為釋放同步狀態的操作會來自多個線程。所以tryReleaseShared(arg)是通過CAS保證的。