來自:泥瓦匠@bysocket.com

https://mp.weixin.qq.com/s/HfyhxqlbXnCXrmFoJHkf9g

本文目錄

  • 一、什麼是 AQS 隊列同步器
  • 二、什麼是 CLH 同步隊列
  • 三、小結

什麼是 AQS ?

Java的內置鎖一直都是備受爭議的,在JDK 1.6之前,synchronized這個重量級鎖其性能一直都是較為低下,雖然在1.6後,進行大量的鎖優化策略,但是與Lock相比synchronized還是存在一些缺陷的:雖然synchronized提供瞭便捷性的隱式獲取鎖釋放鎖機制(基於JVM機制),但是它卻缺少瞭獲取鎖與釋放鎖的可操作性,可中斷、超時獲取鎖,且它為獨占式在高並發場景下性能大打折扣。

在介紹Lock之前,我們需要先熟悉一個非常重要的組件,掌握瞭該組件JUC包下面很多問題都不在是問題瞭。該組件就是AQS。

AQS,AbstractQueuedSynchronizer,即隊列同步器。它是構建鎖或者其他同步組件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC並發包的作者(Doug Lea)期望它能夠成為實現大部分同步需求的基礎。它是JUC並發包中的核心基礎組件。

AQS解決瞭子啊實現同步器時涉及當的大量細節問題,例如獲取同步狀態、FIFO同步隊列。基於AQS來構建同步器可以帶來很多好處。它不僅能夠極大地減少實現工作,而且也不必處理在多個位置上發生的競爭問題。

在基於AQS構建的同步器中,隻能在一個時刻發生阻塞,從而降低上下文切換的開銷,提高瞭吞吐量。同時在設計AQS時充分考慮瞭可伸縮行,因此J.U.C中所有基於AQS構建的同步器均可以獲得這個優勢。

AQS的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態。

AQS使用一個int類型的成員變量state來表示同步狀態,當state>0時表示已經獲取瞭鎖,當state = 0時表示釋放瞭鎖。它提供瞭三個方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))來對同步狀態state進行操作,當然AQS可以確保對state的操作是安全的。

AQS通過內置的FIFO同步隊列來完成資源獲取線程的排隊工作,如果當前線程獲取同步狀態失敗(鎖)時,AQS則會將當前線程以及等待狀態等信息構造成一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程,當同步狀態釋放時,則會把節點中的線程喚醒,使其再次嘗試獲取同步狀態。

AQS主要提供瞭如下一些方法:

  • getState():返回同步狀態的當前值;
  • setState(intnewState):設置當前同步狀態;
  • compareAndSetState(intexpect,intupdate):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性;
  • tryAcquire(intarg):獨占式獲取同步狀態,獲取同步狀態成功後,其他線程需要等待該線程釋放同步狀態才能獲取同步狀態;
  • tryRelease(intarg):獨占式釋放同步狀態;
  • tryAcquireShared(intarg):共享式獲取同步狀態,返回值大於等於0則表示獲取成功,否則獲取失敗;
  • tryReleaseShared(intarg):共享式釋放同步狀態;
  • isHeldExclusively():當前同步器是否在獨占式模式下被線程占用,一般該方法表示是否被當前線程所獨占;
  • acquire(intarg):獨占式獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,將會進入同步隊列等待,該方法將會調用可重寫的tryAcquire(int arg)方法;
  • acquireInterruptibly(intarg):與acquire(int arg)相同,但是該方法響應中斷,當前線程為獲取到同步狀態而進入到同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException異常並返回;
  • tryAcquireNanos(intarg,longnanos):超時獲取同步狀態,如果當前線程在nanos時間內沒有獲取到同步狀態,那麼將會返回false,已經獲取則返回true;
  • acquireShared(intarg):共享式獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨占式的主要區別是在同一時刻可以有多個線程獲取到同步狀態;
  • acquireSharedInterruptibly(intarg):共享式獲取同步狀態,響應中斷;
  • tryAcquireSharedNanos(intarg,longnanosTimeout):共享式獲取同步狀態,增加超時限制;
  • release(intarg):獨占式釋放同步狀態,該方法會在釋放同步狀態之後,將同步隊列中第一個節點包含的線程喚醒;
  • releaseShared(intarg):共享式釋放同步狀態;

CLH同步隊列

那什麼是 CLH同步隊列?

在上面提到瞭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 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同步隊列結構圖如下:

Java 並發之隊列同步器是什麼?

 

入列

學瞭數據結構的我們,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;
 }

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;
 }
 }
 }
 }

在上面代碼中,兩個方法都是通過一個CAS方法compareAndSetTail(Node expect, Node update)來設置尾節點,該方法可以確保節點是線程安全添加的。在enq(Node node)方法中,AQS通過“死循環”的方式來保證節點可以正確添加,隻有成功添加後,當前線程才會從該方法返回,否則會一直執行下去。

過程圖如下:

Java 並發之隊列同步器是什麼?

 

出列

CLH同步隊列遵循FIFO,首節點的線程釋放同步狀態後,將會喚醒它的後繼節點(next),而後繼節點將會在獲取同步狀態成功時將自己設置為首節點,這個過程非常簡單,head執行該節點並斷開原首節點的next和當前節點的prev即可,註意在這個過程是不需要使用CAS來保證的,因為隻有一個線程能夠成功獲取到同步狀態。過程圖如下:

Java 並發之隊列同步器是什麼?

 

代碼示例

本文示例讀者可以通過查看下面倉庫的中的 alibaba/java/ParentClass.java :

  • Github:https://github.com/JeffLi1993/java-core-learning-example
  • Gitee:https://gitee.com/jeff1993/java-core-learning-example

如果您對這些感興趣,歡迎 star、follow、收藏、轉發給予支持!

參考資料

Doug Lea:《Java並發編程實戰》方騰飛:《Java並發編程的藝術》

 

 

❤ 如果喜歡我們的分享,歡迎加入我們,我們是大陸到台灣集運商,現在低價收貨14元每公斤, 時效3天到台灣。如果你有在大陸購物或者進貨的話請資訊我賴: welisen     加我賴吧以後有需可以備用! 

威立森台灣集運


關註官方微信:knifehome


❤ 威立森集運官網 https://www.welisen.com

❤ 淘寶內部優惠券 http://yhq.welisen.com/

 

 

arrow
arrow
    創作者介紹
    創作者 集運賴:welisen 的頭像
    集運賴:welisen

    台灣集運,集運台灣,LINE:welisen 官網 https://www.welisen.com

    集運賴:welisen 發表在 痞客邦 留言(0) 人氣()