【Java併發編程】AQS(1)——併發三板斧

自己定的目標不能一開始就垮了啊,明天就放假了,所以趕緊開始更新Java併發編程系列的第一篇文章(準確來說是第二篇,因爲前面還寫過一篇FutureTask源碼解讀),也是AQS系列的第一篇文章。其實關於AQS系列的早就寫好了,但是一直在反覆修改而沒有發上來,原因是我希望自己的文章是有信息有價值的。作爲一名面向搜索引擎編程的軟件工程幼獅,我每天也會接觸許多無用信息,所以秉着愛護網絡,人人有責的理念,對於以後發出來的文章,我都會認真斟酌檢查

不說廢話了,再說真成信息垃圾了,下面進入正題

 

一.  AQS是什麼

AQS全稱爲AbstractQueuedSynchronizer(後面都以AQS簡稱),翻譯過來叫做抽象隊列同步器,是一個抽象類。我們把它拆開看:抽象、隊列、同步器,這裏我先不解釋,等看完我AQS系列的文章就知道爲什麼要取這個名字了。我們讀源碼很重要的一步就是讀源碼的註釋,所以我將AQS的註釋大致翻譯總結如下:

 

AQS是爲開發人員提供的一種同步框架,它已經幫我們實現了CLH隊列、線程入隊出隊、線程阻塞與喚醒等一系列複雜邏輯,我們只需要根據自己的需要去實現下面相應的方法

  • tryAcquire:獨佔模式下獲取同步狀態

  • tryRelease:獨佔模式下釋放同步狀態

  • tryAcquireShared:共享模式下獲取同步狀態

  • tryReleaseShared:共享模式下釋放同步狀態

  • isHeldExclusively:獨佔模式下,查看當前線程是否獲取同步狀態 

 

AQS提供兩種模式:獨佔模式共享模式

 

獨佔模式意味着每次只有一個線程能拿到鎖並執行相關代碼,而共享模式則意味着同一時間可以有多個線程能拿到鎖來進行協同工作。如果你需要子類是採用獨佔模式,則只需複寫上面的1、2方法;如果想採用共享模式,則只需要複寫3、4方法;第5種方法一般是使用到AQS中的ConditionObject(它就是條件變量)時才重寫

 

AQS其實就是模板模式的經典用例,上面的五個方法就是鉤子方法。concurrent包中的ReentrantLock,CountDownLatch,Semaphor等都是AQS這個"模板"造出來的,所以我們掌握了AQS,基本上concurrent包下其他類的源碼都能很快掌握,下面我們就具體來看看AQS吧

 

二.  什麼是三板斧

 

我們在分析AQS時,先把這三個點重點提出來,這三點是AQS的基石,也是Concurrent包中的基石,我們可以將這三點稱之爲Java併發編程的三板斧:

  1. 狀態:我們AQS及其子類的核心,AQS及其子類所有操作都是依據狀態進進行的。狀態一般會設置成volatile,保證其具有可見性,一定程度上具有有序性

  2. CAS:CAS操作是由Unsafe工具類來實現的,其操作具有原子性,我們一般通過CAS來改變狀態。(狀態被volatile修飾,因此具有可見性和有序性,所以CAS改變狀態時是線程安全的)

  3. 隊列:用來保存等待操作的資源,其數據結構一般爲鏈表。當線程的請求在短時間內得不到滿足時,線程會被包裝成某種類型的數據結構放入隊列中,當條件滿足時則會拿出隊列去重新獲取鎖 

下面我們就一一介紹AQS這裏面的三板斧

 

三.  第一板斧:狀態

 

第一把斧:狀態:

/**
* The synchronization state.
*/
private volatile int state;

在AQS中也叫同步狀態,是被volatile修飾的int型屬性,被CAS操作時是線程安全的。如果是獨佔模式,說明同一時間只有一個線程能拿到鎖執行,所以state只有0和1,爲0時則說明此時有線程已經持有鎖了,而爲1則說明鎖還在,此時可以拿鎖執行。如果是共享模式,則state最大值爲同一時間線程可以拿到鎖的數量。我們在講解AQS的時候其實不會涉及到對這個同步狀態的操作,因爲對這個同步狀態操作一般都是在我們子類實現的模板方法中進行的,所以這個我會在ReentrantLock和Semaphore中說明

 

這個同步狀態是與鎖有關的狀態,我們根據這個狀態來判斷是否還有鎖。而與我們線程相關的狀態,在AQS的內部類Node中,而我們三板斧中的隊列是由Node構成,所以關於線程的狀態我們放在隊列中講解

 

 

四.  第二板斧:CAS

 

我們先貼上AQS中CAS的代碼

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

static {
   try {
       stateOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("state"));
       headOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("head"));
       tailOffset = unsafe.objectFieldOffset
           (AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
       waitStatusOffset = unsafe.objectFieldOffset
           (Node.class.getDeclaredField("waitStatus"));
       nextOffset = unsafe.objectFieldOffset
           (Node.class.getDeclaredField("next"));

   } catch (Exception ex) { throw new Error(ex); }
}


private final boolean compareAndSetHead(Node update) {
   return unsafe.compareAndSwapObject(this, headOffset, null, update);
}


private final boolean compareAndSetTail(Node expect, Node update) {
   return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}


private static final boolean compareAndSetWaitStatus(Node node, int expect, int update) {
   return unsafe.compareAndSwapInt(node, waitStatusOffset, expect, update);
}

private static final boolean compareAndSetNext(Node node, Node expect, Node update) {
   return unsafe.compareAndSwapObject(node, nextOffset, expect, update);
}

首先可以看到有五個long類型的屬性,這五個屬性分別代表state、head、tail,waitStatus、next的偏移量(我們當成地址就行),其中state、head、tail爲AQS的屬性,waitStatus、next爲AQS內部類Node的屬性。我們後面會看到,這幾個屬性都是volatile修飾的,因此可以猜到這些屬性肯定是多線程爭着修改的目標。

 

靜態塊裏則是對這五個屬性偏移量進行初始化

 

至於後面的四個方法最終調用Unsafe類裏面的CAS方法。CAS叫做比較交換操作,我們以下面代碼爲例說下CAS大致的執行流程

unsafe.compareAndSwapObject(this, tailOffset, expect, update);

CAS執行時,會拿地址tailOffset上的值與expect做比較,如果相同,則會將地址上的值更新爲update,並返回true,否則直接返回false。關於CAS更詳細介紹,大家可以網上找找相關資料看下

 

五.  第三板斧:隊列

 

AQS中分爲兩種隊列,一種叫做同步隊列,還有一種叫條件隊列,同步隊列是雙向鏈表結構,條件隊列是單向鏈表,它們都是通過下面要介紹的AQS內部類Node來實現的,下面是Node的代碼


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;

   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() {    // Used to establish initial head or SHARED marker
   }


   Node(Thread thread, Node mode) {     // Used by addWaiter
       this.nextWaiter = mode;
       this.thread = thread;
   }

   Node(Thread thread, int waitStatus) { // Used by Condition
       this.waitStatus = waitStatus;
       this.thread = thread;
   }
}

這個結構其實是一個典型的雙向鏈表結構(同步隊列),既然能做雙向鏈表,肯定也是能做單向鏈表的(條件隊列)。它可以用來保存等待的線程以及線程的狀態等信息。

 

我們先介紹volatile修飾的屬性,被volatile修飾的屬性一般都是通過CAS來操作,也說明這幾個屬性需要在併發情況下保證線程安全

  1. prev:雙向鏈表的前驅節點

  2. next:雙向鏈表的後繼節點

  3. thread:節點所代表的線程

  4. waitStatus:該節點線程所處的狀態,即等待鎖的狀態

 

然後再看下面四個static修飾的屬性

  1. CANCELLED:此節點的線程被取消了

  2. SIGNAL:此節點的後繼節點線程被掛起,需要被喚醒

  3. CONDITION:此節點的線程在等待信號,也表明當前節點不在同步隊列中,而在條件隊列中

  4. PROPAGATE:此節點下一個acquireShared應該無條件傳播

 

這四個屬性就是waitStatus屬性的具體狀態,還有一個隱式的具體狀態,即waitStatus初始化時爲0。在獨佔模式下,我們只需要用到CANCELLED和SIGNAL,這裏需要注意的是SIGNAL,它代表的不是自己線程的狀態,而是它後繼節點的狀態,當一個節點的waitStatus被置爲SIGNAL時,表明此節點的後繼節點被掛起,當此節點釋放鎖或被取消放棄拿鎖時,應該喚醒後繼節點。而在共享模式時,我們會用到CANCELLED和PROPAGATE

 

最後看Node類型的三個屬性

  1. nextWaiter:標記此Node處於何種模式;如果爲null,則爲獨佔模式,爲空Node則爲共享模式

  2. SHARED:nextWaiter的具體狀態,代表共享模式

  3. EXCLUSIVE:nextWaiter的具體狀態,代表獨佔模式

 

好了AQS的第一篇文章併發三板斧就講完啦,後面還有四篇,這幾天會陸續整理放上來。明天就是清明節了,疫情還未結束,大家還是待在家裏休息休息爲國家做做貢獻吧。

 

(未完)

歡迎大家關注我的公衆號 “程序員進階之路”,裏面記錄了一個非科班程序員的成長之路

                                                         

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