AQS實現原理和源碼分析

AQS底層實現原理用一句話總結就是:volatile + CAS + 一個虛擬的FIFO雙向隊列(CLH隊列)。所以在瞭解AQS底層實現時,需要先深入瞭解一下CAS實現原理。

  1. #名詞解釋
  2. 1)CAS:無鎖的策略使用一種比較交換的技術(Compare And Swap)來鑑線程修改衝突,一旦檢測到衝突產生,就重試當前操作直到沒有衝突爲止。
  3. 2)AQS:AbstractQuenedSynchronizer抽象的隊列式同步器,主要提供了一些鎖操作的模板方法。J.U.C都是基於AQS實現的。

1.CAS底層原理

java中CAS操作都是通過Unsafe類實現的,包括一些被廣泛使用的高性能開發庫都是基於Unsafe類開發的,Unsafe類是在sun.misc包下,不屬於Java標準,但是很多Java基礎類庫的CAS操作都是使用Unsafe。包括一些被廣泛使用的高性能開發庫都是基於Unsafe類開發的,比如Netty、Hadoop、Kafka等。

 使用Unsafe的CAS進行一個變量進行修改,實質是直接操作變量的內存地址來實現的,CPU需要通過尋找變量的物理地址來讀取或修改變量。那麼我們需要先了解一下CPU是怎麼尋址物理地址的,這會涉及到計算機底層的段地址、偏移量和邏輯地址(即物理地址)相關概念,下面以8086 CPU處理器爲例來講解。

(1)段地址、偏移量和邏輯地址關係

  1. #段地址、偏移量和邏輯地址(即物理地址)的關係:
  2. 1)關係:8086 CPU的邏輯地址由段地址和段內偏移量兩部分組成
  3. 物理地址 = 段地址*16 + offset偏移量。
  4. 2)段地址
  5. 8086 CPU能提供20位的地址信息,可直接對1M(1M = 1024 * 1024 = 2^20)個存儲單元進行訪問,而CPU內部可用來
  6. 提供地址信息的寄存器都是16位,那怎樣用16位寄存器來實現20位地址尋址呢? 8086 CPU的20位的地址信息可以對1M個內存單元進行訪問,
  7. 也就是說編址00000H~FFFFFH,而段寄器CS,DS,SS,ES即存放了這些地址的高4位。
  8. 如:123456H,則某個段寄存器便會存儲1234H高4位信息,這即爲段地址。
  9. 3)偏移量:段內偏移地址就是移動後相對於段地址的偏移量。
  10. 4)邏輯地址(物理地址):物理地址就是地址總線上提供的20位地址信息。
  11. 物理地址 = 段地址*10H + 段內偏移地址。段地址乘以10H是因爲段地址當時是取高四位得到的,
  12. 所以還原後要讓段地址左移4位(10H = 10000B)。
  13. 例如:(cs)= 20A8H,(offset)=2008H,則物理地址爲20A8H*10H+2008H = 22A88H。

(2)Unsafe中C2AS實現原理

Unsafe的CAS操作方法基本都是native方法,具體的實現是由C實現的,下面以compareAndSwapInt()方法爲例看看處理器低層是怎麼實現CAS操作的。

  1. #1.源碼
  2. public final native boolean compareAndSwapInt(Object o,long offset,int expect,int update)
  3. #2.調用:該方法一般的調用形式是:object對象傳的是this
  4. unsafe.compareAndSwapInt(this, Offset, expect, update)

CAS操作的比較原理:

  • 1)CPU的尋址方式:處理器會找到this寄存器cs裏的段地址,通過段地址*16 + Offset偏移量得到物理地址。
  • 2)將物理地址處的存儲值 和 期望值expect比較,如果相等,則進行update操作後返回true;不相等返回false。
  • 3)比較操作 和 更新賦值操作是原子進行的,CPU處理器是通過lock鎖實現的(總線鎖和緩存鎖)

CAS操作在intel X86處理器的源代碼的片段

  1. inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,jint compare_value) {
  2. int mp = os::is_MP();
  3. __asm {
  4. mov edx, dest
  5. mov ecx, exchange_value
  6. mov eax, compare_value
  7. #程序會根據當前處理器的類型來決定是否爲cmpxchg指令添加lock前綴,lock鎖的實現:總線鎖和緩存鎖
  8. LOCK_IF_MP(mp)
  9. cmpxchg dword ptr [edx], ecx
  10. }
  11. }

CAS操作指令解析

  • 1)is_MP()函數判斷當前處理器是多核,還是單核。
  • 2)如果程序是在多處理器上運行,就爲cmpxchg指令加上lock前綴(Lock Cmpxchg)。如果程序是在單處理器上運行,就省略lock前綴。

intel處理器lock前綴作用

  • 1)確保對內存的讀-改-寫操作原子執行。處理器會使用總線鎖 或 緩存鎖來保持原子性。
  • 2)禁止該指令,與之前和之後的讀和寫指令重排序。
  • 3)把CPU寫緩衝區中的所有數據刷新到內存中,使其它CPU緩存失效。

2.AQS實現原理

AQS全稱爲AbstractQueuedSynchronizer,AQS定義了一套多線程訪問共享資源的同步器框架,爲java併發同步組件提供統一的底層支持。常見的有:Lock、ReentrantLock、ReentrantReadWriteLock、CyclicBarrier、CountDownLatch、ThreadPoolExecutor等都是基於AQS實現的。

AQS是一個抽象類,主要是通過繼承的方式實現其模版方法,它本身沒有實現任何的同步接口,僅僅是定義了同步狀態的獲取以及釋放的方法來提供自定義的同步組件。

(1)AQS的獨佔鎖和共享鎖

  • 獨佔鎖:每次只能有一個線程持有鎖,比如ReentrantLock就是以獨佔方式實現的互斥鎖。

  • 共享鎖:允許多個線程同時獲取鎖,併發訪問共享資源,比如ReentrantReadWriteLock。

(2)AQS內部實現

AQS的實現依賴內部的FIFO的雙向隊列同步(只是一個虛擬的雙向隊列)和共享鎖state變量。當線程競爭鎖(state變量)失敗時,就會把AQS把當前線程封裝成一個Node加入到同步隊列中,同時再阻塞該線程;當獲取鎖的線程釋放鎖以後,會從隊列中喚醒一個阻塞的節點(線程)。 ASQ具體實現如下圖:

  1. #1.AQS底層實現原理
  2. volatile + CAS + 一個虛擬的FIFO雙向隊列(CLH隊列)。
  3. #2.AQS同步隊列特點:
  4. 1)AQS同步隊列內部維護的是一個FIFO的雙向鏈表,雙向鏈表可以從任意一個節點開始很方便的訪問前驅和後繼。
  5. 2)添加節點:每個Node其實是由線程封裝的(node裏面包含一個thread變量),當線程競爭鎖失敗後會封裝成Node加入到ASQ同步隊列尾部。
  6. 3)移除節點:AQS同步隊列中每個node都在等待同一個資源(鎖狀態state變量)釋放,鎖釋放後每次只有隊列的第二個節點(head的後繼節點)纔有機會搶佔鎖,如果成功獲取鎖,則此節點晉升爲頭節點。

(3)AQS的源碼分析

爲了方便後面的源碼理解,我們先了解一下AQS和Node類的主要內部變量的功能。

  1. //1.AQS的主要結構
  2. public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer{
  3. //AQS的雙向鏈表隊列的頭節點
  4. private transient volatile Node head;
  5. //AQS的雙向鏈表隊列的未節點
  6. private transient volatile Node tail;
  7. //鎖的狀態
  8. private volatile int state;
  9. //AQS的內部類node節點,由線程封裝而來的
  10. static final class Node {}
  11. }
  12. //2.Node類的內部結構
  13. static final class Node {
  14. //共享鎖
  15. static final Node SHARED = new Node();
  16. //獨佔鎖
  17. static final Node EXCLUSIVE = null;
  18. //節點等待狀態-取消狀態:因爲超時或者中斷,節點會被設置爲取消狀態,被取消的節點時不會參與到鎖競爭中去,它會一直處於取消狀態不會轉變爲其他狀態。
  19. static final int CANCELLED = 1;
  20. //節點等待狀態-等待狀態:後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行。
  21. static final int SIGNAL = -1;
  22. //節點等待狀態-條件等待狀態:節點在等待隊列Condition中,當Condition調用了signal()後節點將會從等待隊列中轉移到同步隊列中,參與鎖獲取。
  23. static final int CONDITION = -2;
  24. //表示下一次共享式同步狀態獲取將會無條件地傳播下去
  25. static final int PROPAGATE = -3;
  26. //等待狀態
  27. volatile int waitStatus;
  28. //等待隊列中的後繼節點
  29. Node nextWaiter;
  30. //前驅節點
  31. volatile Node prev;
  32. //後繼節點
  33. volatile Node next;
  34. //獲取鎖狀態的線程
  35. volatile Thread thread;
  36. }

爲了弄清楚AQS的基本框架,我們以ReentrantLock的非公平鎖爲例來分析AQS的源碼。先通過一個示例看看ReentrantLock鎖是怎麼使用的,後面我們再進一步分析ReentrantLock加鎖 和 解鎖的過程。

  1. @Test
  2. public void testLock() {
  3. ReentrantLock lock = new ReentrantLock();
  4. try {
  5. lock.lock();
  6. System.out.println("其他同步業務操作");
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }finally {
  10. lock.unlock();
  11. }
  12. }

ReentrantLock構造器

  1. //1.默認情況下ReentrantLock使用的非公平鎖NonfairSync
  2. public ReentrantLock() {
  3. sync = new NonfairSync();
  4. }
  5. //2.可以指定ReentrantLock使用NofairSync(非公平鎖) 還是 FailSync(公平鎖)
  6. public ReentrantLock(boolean fair) {
  7. sync = fair ? new FairSync() : new NonfairSync();
  8. }

ReentrantLock中的非公平鎖(NofairSync)、公平鎖(FailSync)都是Sync這個類的實現,Sync又是繼承了AQS抽象類。

  • 公平鎖:表示所有線程嚴格按照FIFO來獲取鎖。
  • 非公平鎖:表示可以存在搶佔鎖的功能,也就是說不管當前AQS同步隊列上是否存在其他線程等待,新加入的線程都有機會搶佔鎖。

1)加鎖過程

ReentrantLock中非公平鎖的加鎖lock()方法源碼調用過程的時序圖如下:

從上圖可以看出,當鎖獲取失敗時,會調用addWaiter()方法將當前線程封裝成Node節點加入到AQS同步隊列尾部;然後再調用acquireQueued()方法將加入尾部隊列的node進行阻塞。

ReentrantLock.lock()源碼

  1. //ReentrantLock加鎖時,獲取鎖的入口是調用抽象類sync裏面的方法。sync的具體實現在ReentrantLock構造時已經指定實例:NofairSync(非公平鎖) 或 FailSync(公平鎖)。
  2. public void lock() {
  3. sync.lock();
  4. }

NofairSync.lock()源碼實現

  1. //(1)獲取鎖lock()
  2. final void lock() {
  3. //非公平鎖只要加鎖,當前線程就會去競爭鎖state,通過compareAndSetState()嘗試鎖的競爭
  4. if (compareAndSetState(0, 1))
  5. //獲取鎖成功,設置鎖擁有的線程
  6. setExclusiveOwnerThread(Thread.currentThread());
  7. else
  8. //獲取失敗
  9. acquire(1);
  10. }
  11. //(2)使用CAS進行獲取鎖的狀態,stateOffset是AQS裏鎖狀態state的偏移地址
  12. protected final boolean compareAndSetState(int expect, int update) {
  13. //原子性操作:通過cas樂觀鎖的方式來做比較並替換。如果當前內存中的state的值和預期值expect相等,則替換爲update。更新成功返回true,否則返回false。
  14. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
  15. }
  • (1)鎖狀態state在AQS是一個volatile的int變量,當state=0時,表示鎖沒被佔用,其它線程可以獲取鎖資源;state>0時,表示鎖已經被佔用。AQS使用了volatile+CAS來保證鎖狀態state的原子性和可見性。

  • (2)ReentrantLock是可重入鎖,同一個線程多次獲得同步鎖的時候,state會遞增;釋放鎖時state會遞減。比如重入3次,那麼state=3,對應釋放鎖也會釋放3次直到state=0後,其他線程纔有資格獲取鎖。

acquire(int arg)源碼

主要完成了鎖狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作。同步鎖狀態獲取流程,也就是acquire(int arg)方法調用流程如下圖:

  • (1)調用tryAcquire再嘗試鎖的獲取。

  • (2)如果獲取失敗調用addWaiter將當前線程封裝成node加到AQS同步隊列的尾部。

  • (3)最後再調用acquireQueued使節點以自旋方式來獲取鎖狀態。如果獲取不到則阻塞當前節點中的線程,而阻塞線程的喚醒主要依靠前驅節點的出隊 或 阻塞線程被中斷來實現。

  1. public final void acquire(int arg) {
  2. if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
  3. selfInterrupt();
  4. }

tryAcquire()源碼

  • (1)tryAcquire方法在AQS中是個模板方法,具體實現在NonfairSync中。

  • (2)方法主要作用是判斷state鎖是否被佔用,如果沒被佔用會用CAS嘗試獲取鎖;如果被佔用了再判斷是否是同一個線程鎖重入,如果是重入鎖就將重入鎖的次數加1。

  1. protected final boolean tryAcquire(int acquires) {
  2. return nonfairTryAcquire(acquires);
  3. }
  4. final boolean nonfairTryAcquire(int acquires) {
  5. //先獲取當前線程
  6. final Thread current = Thread.currentThread();
  7. //獲取鎖state狀態
  8. int c = getState();
  9. //如果鎖state=0說明鎖沒被佔用,再用CAS嘗試修改鎖的狀態來獲取鎖
  10. if (c == 0) {
  11. if (compareAndSetState(0, acquires)) {
  12. //獲取鎖成功後,設置鎖擁有的線程
  13. setExclusiveOwnerThread(current);
  14. return true;
  15. }
  16. }
  17. //鎖state被佔用時,如果是同一個線程多次重入鎖,則直接增加重入次數
  18. else if (current == getExclusiveOwnerThread()) {
  19. int nextc = c + acquires; //增加重入次數
  20. if (nextc < 0) // overflow溢出int的最大值時,拋出異常
  21. throw new Error("Maximum lock count exceeded");
  22. //重新設置鎖state值
  23. setState(nextc);
  24. return true;
  25. }
  26. return false;
  27. }

addWaiter()源碼

  • (1)addWaiter()方法是將當前線程封裝成node,然後通過自旋使用CAS操作添加到AQS同步隊列的尾部(tail)。

  • (2)如果是AQS同步隊列爲空,表示第一次添加node,需要初始化AQS同步隊列,即初始化隊列的head頭;如果是cas失敗,則調用enq自旋將節點添加到AQS同步隊列。

  1. private Node addWaiter(Node mode) {
  2. //將當前線程組裝成一個node
  3. Node node = new Node(Thread.currentThread(), mode);
  4. Node pred = tail;
  5. //判斷AQS同步隊列是否需要初始化,只有第一次添加node時需要初始化head節點。
  6. if (pred != null) {
  7. //如果AQS不是空隊列,會將新的node節點通過CAS添加到隊裏的尾部
  8. node.prev = pred;
  9. if (compareAndSetTail(pred, node)) {
  10. pred.next = node;
  11. return node;
  12. }
  13. }
  14. //如果隊列爲空或者cas失敗,進入enq初始化隊列或將節點添加到AQS同步隊列中
  15. enq(node);
  16. return node;
  17. }
  18. //初始化隊列或通過自旋將node添加到隊列的尾部
  19. private Node enq(final Node node) {
  20. //自旋添加節點到隊列尾部
  21. for (;;) {
  22. Node t = tail;
  23. //AQS爲空時使用CAS初始化隊列
  24. if (t == null) {
  25. if (compareAndSetHead(new Node()))
  26. tail = head;
  27. } else {
  28. //隊列不爲空就將node節點追加到AQS同步隊列的尾部
  29. node.prev = t;
  30. if (compareAndSetTail(t, node)) {
  31. t.next = node;
  32. return t;
  33. }
  34. }
  35. }
  36. }

addWaiter通過自旋向隊列中添加節點時,會涉及到三步操作,例如下圖向只有兩個node的AQS同步隊列中添加一個node3

  • 第一步:先將舊隊列的尾節點node2賦給新加節點node3的前驅prev,即node3.prev指向節點node2。

  • 第二步:再通過CAS操作,將tail重新指向新加節點node3。

  • 第三步:如果上面的CAS成功,將舊隊列尾節點node2的next指向新加節點node3,即完成雙向指針操作。

acquireQueued()源碼

節點進入同步隊列之後,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的線程)。執行完acquireQueued()方法後AQS同步隊列節點自旋獲取同步狀態如下圖:

  • (1)acquireQueued方法主要是進行自旋搶佔鎖的操作 ,如果當前節點node的前驅節點搶佔鎖失敗時,會根據前驅節點等待狀態(waitStatus)來決定是否需要掛起線程。

  • (2)每個節點線程在“自旋”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取鎖的同步狀態(state)。

  • (3)如果搶佔鎖的操作拋出異常,會通過cancelAcquire方法取消獲得鎖的操作,並將當前node進行出隊操作。

  1. final boolean acquireQueued(final Node node, int arg) {
  2. //操作失敗標記,操作出現異常時需要將node移除隊列
  3. boolean failed = true;
  4. try {
  5. //中斷標記位
  6. boolean interrupted = false;
  7. //自旋
  8. for (;;) {
  9. //獲取當前節點的前驅prev節點
  10. final Node p = node.predecessor();
  11. //如果前驅prev節點是head節點時,纔有資格進行鎖搶佔
  12. if (p == head && tryAcquire(arg)) {
  13. //前驅prev節點搶佔鎖成功後,重新設置head頭:將舊head的後繼節點next設置爲新head頭,所以鎖釋放後每次只有隊列的第二個節點(head的後繼節點)纔有機會搶佔鎖。
  14. setHead(node);
  15. //斷開舊head節點:凡是head節點head.thread與head.prev永遠爲null, 但是head.next不爲null,所以只需要斷開head.next。
  16. p.next = null; // help GC
  17. failed = false;
  18. return interrupted;
  19. }
  20. //如果獲取鎖失敗,會根據節點等待狀態waitStatus來決定是否掛起線程
  21. if (shouldParkAfterFailedAcquire(p, node) &&
  22. parkAndCheckInterrupt())//若前面爲true,則執行掛起,待下次喚醒的時候會檢測中斷的標誌
  23. interrupted = true;
  24. }
  25. } finally {
  26. //如果拋出異常則取消鎖的獲取,再將node進行出隊操作
  27. if (failed)
  28. cancelAcquire(node);
  29. }
  30. }
  31. //(1).shouldParkAfterFailedAcquire()方法主要作用是:把隊列中node前的CANCELLED的節點給剔除掉。
  32. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  33. //獲取node前繼節點pred的等待狀態
  34. int ws = pred.waitStatus;
  35. //如果是SIGNAL狀態,意味着node前繼節點的線程需要被unpark喚醒
  36. if (ws == Node.SIGNAL)
  37. return true;
  38. //如果node前繼節點pred的等待狀態大於0,即爲CANCELLED狀態時,則會從pred節點往前一直找到一個沒有被CANCELLED的節點設置爲pred,即當前node節點的前驅節點。在尋找的過程中會把隊列中CANCELLED的節點剔除掉(下面會用圖進行講解)
  39. if (ws > 0) {
  40. do {
  41. node.prev = pred = pred.prev;
  42. } while (pred.waitStatus > 0);
  43. pred.next = node;
  44. } else {
  45. //如果node的前繼節點pred爲初始狀態0或者“共享鎖”狀態,則設置前繼節點爲SIGNAL狀態。
  46. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  47. }
  48. return false;
  49. }
  50. //(2).parkAndCheckInterrupt()阻塞當前線程,等待喚醒的時候再檢測中斷的標誌
  51. private final boolean parkAndCheckInterrupt() {
  52. //park阻塞當前node線程,等待unpark喚醒
  53. LockSupport.park(this);
  54. //線程被喚醒時,再判斷是否是中斷狀態
  55. return Thread.interrupted();
  56. }

shouldParkAfterFailedAcquire()方法中while循環的作用是:從當前節點的node.prev向前遍歷,一直找到一個沒有被CANCELLED的節點,在尋找過程中會把狀態爲CANCELLED的節點給剔除掉。如下圖當前節點是node4,AQS同步隊列裏節點node2是取消狀態(CANCELLED)時,就會進入該while循環移除掉節點node2。

到此ReentrantLock加鎖的整個過程就分析完了,在加鎖過程中有幾個地方需要注意

  • (1)當同一個線程多次重入鎖,直接增加重入次數,即將鎖的狀態state加1。

  • (2)addWaiter使用自旋 + CAS將新node添加到AQS同步隊列中,其中自旋的目的是爲了防止CAS失敗。

  • (3)每次只有head節點纔有資格進行搶佔鎖資源(state),head釋放鎖後只有隊列的第二個節點(head的後繼節點)纔有機會搶佔鎖。

  • (4)節點會根據等待狀態(waitStatus)來決定是否掛起線程,在進行掛起線程操作時,會移除掉狀態爲CANCELLED的節點。

獲取鎖過程總結:在獲取同步狀態時,AQS同步器維護一個同步隊列,獲取鎖狀態失敗的線程都會被加入到隊列尾部、並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點成爲頭節點、且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

2)解鎖過程

相對於加鎖過程,解鎖就更爲簡單了,ReentrantLock中非公平鎖的解鎖unlock()方法調用的時序圖如下:

ReentrantLock.unlock()源碼

  1. public void unlock() {
  2. //將鎖的狀態state減1
  3. sync.release(1);
  4. }

release()源碼

調用這個release()方法幹了兩件事:1.釋放鎖 ;2.喚醒park的線程。

  1. public final boolean release(int arg) {
  2. //嘗試釋放鎖state
  3. if (tryRelease(arg)) {
  4. //如果釋放鎖成功,喚醒park的線程
  5. Node h = head;
  6. if (h != null && h.waitStatus != 0) //如果head是初始化節點時,不需要喚醒其他線程
  7. //通過unpark喚醒一個阻塞線程
  8. unparkSuccessor(h);
  9. return true;
  10. }
  11. return false;
  12. }

tryRelease()源碼

tryRelease釋放鎖時,如果是鎖重入情況下釋放鎖,則減少鎖state的重入次數(即減少state的值),直到鎖的狀態state=0時,才真正的釋放掉鎖資源,其他線程纔能有資格獲取鎖。例如一個線程重入鎖3次,即鎖狀態state=3,每執行tryRelease一次就釋放鎖一次state就減1,直到state=0時鎖資源才真正釋放掉。

  1. protected final boolean tryRelease(int releases) {
  2. //同一個線程每釋放一次鎖state就減1
  3. int c = getState() - releases;
  4. if (Thread.currentThread() != getExclusiveOwnerThread())
  5. throw new IllegalMonitorStateException();
  6. //鎖釋放標誌
  7. boolean free = false;
  8. //鎖state=0時才真正釋放掉鎖,將鎖持有線程設置爲null
  9. if (c == 0) {
  10. free = true;
  11. setExclusiveOwnerThread(null);
  12. }
  13. //更新鎖的狀態(不需要CAS操作,因爲釋放鎖操作是已經獲得鎖的情況下進行的)
  14. setState(c);
  15. return free;
  16. }

unparkSuccessor()源碼

unparkSuccessor就是真正要釋放了後,傳入head節點喚醒下一個線程。線程喚醒邏輯可以總結成一下幾點:

  • (1)如果head節點其後繼next節點waitStatus在等待喚醒狀態(SIGNAL),則直接unpark後繼節點。

  • (2)如果head節點其後繼next節點waitStatus不是在等待狀態(SIGNAL),就從隊列尾部向前遍歷找到一個waitStatus在等待喚醒狀態的節點。留一個思考:爲什麼是從隊列尾部向前遍歷,而不是從前向尾部遍歷?

  1. //傳入的參數node就是head節點
  2. private void unparkSuccessor(Node node) {
  3. //獲取節點的等待狀態
  4. int ws = node.waitStatus;
  5. if (ws < 0)
  6. compareAndSetWaitStatus(node, ws, 0);
  7. Node s = node.next;
  8. //如果head的後繼節點waitStatus爲取消狀態(CANCELLED)時,進行從隊列尾部向前遍歷尋找等待狀態的node
  9. if (s == null || s.waitStatus > 0) {////判斷後繼節點是否爲空或者是否是取消狀態
  10. s = null;
  11. //循環遍歷
  12. for (Node t = tail; t != null && t != node; t = t.prev)
  13. if (t.waitStatus <= 0)
  14. s = t;
  15. }
  16. //unpark喚醒下一個線程
  17. if (s != null)
  18. LockSupport.unpark(s.thread);
  19. }

unparkSuccessor()方法裏循環遍歷從隊列尾部向前遍歷原因是防止死循環。因爲在鎖競爭acquireQueued()方法中,異常處理cancelAcquire()方法中最後的node.next = node操作,會出現如下圖的環狀結構導致死循環。

到此lock加鎖和解鎖的源碼就分析結束了,看了這麼多AQS源碼最值得借鑑的兩個思路就是:第一使用了CAS + volatile來保證鎖state操作的原子性和可見性;第二使用一個虛擬的FIFO雙向隊列來解決線程衝突問題。

總結
通過這篇文章基本將AQS隊列的實現過程做了比較清晰的分析,主要是基於非公平鎖的獨佔鎖實現。在獲得同步鎖時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章