原始碼分析:同步基礎框架——AbstractQueuedSynchronizer(AQS)

Admol發表於2020-11-17

簡介

AQS 全稱是 AbstractQueuedSynchronizer,位於java.util.concurrent.locks 包下面,AQS 提供了一個基於FIFO的佇列和維護了一個狀態state變數賴表示狀態,可以作為構建鎖或者其他相關同步裝置的基礎框架。AQS 支援兩種模式:共享模式 和 排他模式,當它被定義為一個排他模式時,其他執行緒對其的獲取就被阻止,而共享模式對於多個執行緒獲取都可以成功。之所以說它是一個同步基礎框架是因為很多同步類裡面都用到了AQS,比如 ReentrantLock 中的內部類同步器Sync繼承至AQS,ReentrantReadWriteLock中的同步器也是繼承至AQS,還有 Semaphore 、CountDownLatch等都是基於AQS來實現的。

核心原始碼

類結構

AQS 繼承了 AbstractOwnableSynchronizer, AbstractOwnableSynchronizer 這個類比較簡單,就一個屬性 private transient Thread exclusiveOwnerThread ,用來標識當前獨佔鎖的持有者執行緒,通俗的說就是哪個執行緒拿到了獨佔鎖,就呼叫AbstractOwnableSynchronizer 的方法把這個執行緒儲存起來。原始碼如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
	...
}

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
  private transient Thread exclusiveOwnerThread;
  // 構造方法,get set 方法省略。。。
}

後面的分析中,會有大量的同步器在獲得鎖之後會呼叫setExclusiveOwnerThread(Thread) 方法來儲存鎖的持有者執行緒;

重要內部類Node

static final class Node {
	volatile int waitStatus;
	volatile Node prev;
	volatile Node next;
	volatile Thread thread;
	Node nextWaiter;
}

以上五個成員變數主要負責儲存該節點的執行緒引用,同步佇列的前驅和後繼節點,同時也包括了同步狀態。

屬性解釋:

waitStatus:表示節點的狀態。其中包含的狀態有:

  1. CANCELLED,值為1,表示當前的執行緒被取消;
  2. SIGNAL,值為-1,表示當前節點的後繼節點包含的執行緒需要執行,也就是unpark;
  3. CONDITION,值為-2,表示當前節點在等待condition,也就是在condition佇列中;
  4. PROPAGATE,值為-3,表示當前場景下後續的acquireShared能夠得以執行;
  5. 值為0,表示當前節點在sync佇列中,等待著獲取鎖。

prev:前驅節點,比如當前節點被取消,那就需要前驅節點和後繼節點來完成連線

next:後繼節點

thread:入佇列時的當前執行緒

nextWaiter:儲存condition佇列中的後繼節點

重要屬性:同步佇列和同步狀態

節點成為同步佇列和 condition 條件佇列構建的基礎,同步器擁有三個成員變數:頭結點head、尾節點tail和同步狀態state

private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;

對於新的獲取鎖請求,形成Node節點,掛載到佇列的尾部;對於鎖資源的釋放都是從佇列的頭部進行操作的。

      +------+  prev +-----+       +-----+
 head |      | <---- |     | <---- |     |  tail
      +------+       +-----+       +-----+

可以重寫的API

實現自定義同步器時,需要使用同步器提供的getState()、setState()和compareAndSetState()方法來控制同步狀態。

方法1:protected boolean tryAcquire(int arg)

描述:已排它模式獲取同步狀態。這個方法的實現需要查詢前狀態是否允許獲取,然後再進compareAndSetState()修改狀態,修改成功代表成功獲得鎖。

方法2:protected boolean tryRelease(int arg)

描述:釋放鎖,也就是釋放同步狀態state的值到初始狀態,一般是0。

方法3:protected int tryAcquireShared(int arg)

描述:共享模式下獲取同步狀態,一般可以用來做共享鎖,或者用作限制資源最多同時被訪問多少次。

方法4:protected boolean tryReleaseShared(int arg)

描述:共享模式下釋放同步狀態。

方法5:protected boolean isHeldExclusively()

描述:在排它模式下,返回同步狀態是否被佔用,比如我們可以實現返回邏輯為 getState() == 1,為true的話說明資源已經被佔用了。

其他程式碼我們通過自己實現簡單的排他鎖案例來進行具體的詳細分析

基於AQS實現的排他鎖

一、定義一個MyAQSLock類

public class MyAQSLock{
}

二、定義一個內部類Sync做為同步器,繼承自AbstractQueuedSynchronizer

public class MyAQSLock{
	class Sync extends AbstractQueuedSynchronizer{
	}
}

三、重寫同步器部分API

因為我們要實現的是排它鎖的功能,意思就是同一時刻只能有一個執行緒獲得鎖,所以只需要重寫tryAcquire、tryReleaseisHeldExclusively方法即可。

class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean **tryAcquire**(int acquires){
            // 入參只能為1
            **assert acquires == 1;
            // 使用CAS的方式修改state值,修改成功代表成功獲得鎖
            if(compareAndSetState(0,1)){
                // 修改鎖的持有者為當前執行緒
                setExclusiveOwnerThread(Thread.currentThread());
                // 返回true,表示成功獲得鎖
                return true;
            }
            // 返回false,沒有獲得鎖
            return false;
        }

        @Override
        protected boolean **tryRelease**(int releases){
            assert releases == 1;
            if (getState() == 0){
                // 已經被釋放了
                throw new IllegalMonitorStateException();
            }
            // lock() 和 unlock() 一般都是成對出現的,所以這裡不需要同步語句,可以直接修改state值為0
            setState(0);
            return true;
        }
        @Override
        protected boolean **isHeldExclusively**() {
            // 返回true,說明已經有其他執行緒獲得鎖
            return getState() == 1;
        }

    }

三、定義鎖和解鎖方法

public class MyAQSLock{

    private final Sync sync;
    MyAQSLock(){
        sync = new Sync();
    }
		class Sync extends AbstractQueuedSynchronizer{
		...
		}
    public void lock(){
        // 呼叫同步器,獲得鎖
        sync.acquire(1);
    }

    public boolean tryLock(){
			  // 嘗試獲得鎖,如果沒有獲取到鎖,則立即返回false
        return sync.tryAcquire(1);
    }
 
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException{
        // 嘗試獲得鎖,如果沒有獲取到鎖,允許等待一段時間
        return sync.tryAcquireNanos(1,unit.toNanos(timeout));
    }
 
    public void unLock(){
        // 解鎖
        sync.release(1);
    } 

    public boolean isLocked(){
        // 判斷鎖是否已經被佔用
        return sync.isHeldExclusively();
    }
}

四、測試我們的鎖

static int count = 0;
public static void main(String[] args) throws InterruptedException{
    MyAQSLock myAQSLock = new MyAQSLock();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    IntStream.range(0,1000).forEach(i->new Thread(()->{
        myAQSLock.lock();
        try{
            IntStream.range(0,10000).forEach(j->{
                count++;
            });
        }finally{
            myAQSLock.unLock();
        }
        countDownLatch.countDown();
    }).start());
    countDownLatch.await();
    System.out.println(count);
}

最後正確輸出10000000,說明我們實現的鎖是有效的。但是要注意我們自己寫的這個鎖是不支援重入的。

程式碼實現分析

獲得鎖:public void lock()

lock()方法會呼叫sync.acquire(int)方法,acquire在AQS裡面,方法被final修飾,作為基礎框架邏輯部分,不允許被繼承,原始碼展示:

public final void acquire(int arg) {
    // tryAcquire 是我們自己實現的方法,具體實現看上面
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // acquireQueued 返回true表示執行緒被中斷了,中斷當前執行緒
        selfInterrupt();
}

上面acquire()主要的邏輯有:

  1. 嘗試獲得鎖,呼叫tryAcquire(arg)方法,該方法的邏輯在我們自定義的MyAQSLock類中,我們利用了compareAndSetState來保證state欄位的原子性。

  2. 如果tryAcquire返回true的話,if分支會直接退出,表示成功獲得鎖,繼續執行呼叫lock() 方法後面的邏輯;

  3. 如果tryAcquire返回false的話,表示沒有獲得鎖,會繼續執行 && 後面的邏輯;

  4. 首先會呼叫addWaiter(Node.EXCLUSIVE)方法為當前執行緒建立排隊節點,並加入到佇列,Node.EXCLUSIVE代表這個節點是獨佔排他鎖的意思,具體原始碼如下:

    private Node addWaiter(Node mode) {
        // 為當前執行緒建立一個節點,最後會返回出去這個節點
        Node node = new Node(Thread.currentThread(), mode);
        // 佇列不為空時,快速嘗試在同步佇列尾部新增當前節點,如果失敗了會進入enq方法自旋入隊
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 上面入隊失敗了,或者是pred為空(第一個排隊的執行緒進來),繼續自旋入隊
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        // 空的for迴圈,自旋操作,直到成功把節點加入到同步佇列
        for (;;) {
            // 同步佇列尾巴
            Node t = tail;
            if (t == null) { // Must initialize
                 // 尾巴是空的,還沒有初始化, 第一個排隊的執行緒進來的話,隊頭隊尾都是同一個節點
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 進入到這裡,說明同步佇列已經有執行緒在排隊了
                // 當前節點前驅直接指向同步隊裡的尾節點
                node.prev = t;
                // CAS 修改尾節點為當前節點
                if (compareAndSetTail(t, node)) {
                    // t還是老的尾節點,修改新的尾節點後老的尾節點的下一個節點就是當前節點,建立他們的聯絡
                    t.next = node;
                    // 成功把當前節點加入到了同步佇列,返回當前節點,退出自旋
                    return t;
                }
            }
        }
    }
    

    addWaiter()方法總結:首先會快速嘗試一次在佇列的尾部新增當前執行緒節點,如果失敗的話(在這個時候,可能有新的執行緒也沒有獲得鎖,並且跑在當前的前面加入到同步佇列了),會呼叫enq邏輯進行自旋加入隊尾,直到成功加入佇列為止。

  5. 再次嘗試從同步佇列獲得鎖acquireQueued(node,arg)

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋操作
            for (;;) {
                // 當前節點的上一個節點
                final Node p = node.predecessor(); //**①**
                // 如果前驅節點是頭結點,然後去嘗試獲得鎖,tryAcquire是我們自己實現的獲得鎖邏輯
                **if (p == head && tryAcquire(arg)) { //②**
                    // 當前執行緒成功獲得鎖,當前節點設定為頭結點
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 返回false,表示沒有被中斷
                    return interrupted;
                }
                // 到這裡說明p != head 或者 **tryAcquire** 返回了false,還是沒獲得鎖,這時候就需要阻塞執行緒了
                // shouldParkAfterFailedAcquire 如果執行緒應阻塞,則返回true
                // parkAndCheckInterrupt  阻塞當前執行緒 
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 節點被取消了
            if (failed)
                cancelAcquire(node);
        }
    }
    
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
         // SIGNAL值為-1,表示pred節點的後繼節點包含的執行緒需要執行,也就是unpark
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            // 大於0的值只有1, 1表示執行緒被取消
            // 進入到這裡說明 pred 節點被取消了,需要從同步佇列上刪掉它
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            // 一般初始時為0,設定成-1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false,外面會一直自旋操作
        return false;
    }
    
    private final boolean parkAndCheckInterrupt() {
        // 呼叫底層的Unsafe一直阻塞執行緒
        LockSupport.park(this);
        // 被unpark喚醒之後,會繼續回去自旋獲得鎖,並返回執行緒在此期間是否有被中斷
        return Thread.interrupted();
    }
    

    lock方法總結:

    1. 嘗試獲得鎖,方法tryAcquire
      1. 成功獲得鎖,直接退出;沒有獲得鎖,繼續執行;
    2. 新建排隊節點,並加入到同步佇列,方法addWaiter
      1. 佇列不為空時,嘗試一次快速直接把節點加入到佇列尾巴上;如果隊尾為空或者快速新增失敗,繼續執行下面邏輯
      2. 自旋,直到成功把新建的節點加入到同步佇列;方法enq
        為什麼要自旋呢?是因為在調這個方法的時候,可能有其他想要獲得鎖執行緒沒有獲得鎖,並且已經修改了尾節點;
    3. 再次嘗試從同步佇列獲得鎖,方法acquireQueued
      1. 上面已經把當前執行緒的節點加入到佇列中了,理論上排隊的執行緒很多的話,它是馬上獲取不到鎖的
      2. 所以它會自旋判斷是否到了自己可以獲取鎖和CAS嘗試獲取鎖,關鍵程式碼if (p == head && tryAcquire(arg))
        理論上當前執行緒進入到了佇列排隊,只要佇列中還有更早的執行緒在它前面排隊,當前執行緒都不會比更早的執行緒先獲得鎖,所以在這一塊對於公平鎖和非公平鎖肯定都是公平的。
      3. 沒有資格去獲取鎖或沒有成功獲得鎖,就阻塞自己,方法parkAndCheckInterrupt
      4. 阻塞執行緒被喚醒,自旋成功獲得鎖,排隊期間被中斷的執行緒也會獲得鎖,之後退出自旋迴圈,返回執行緒的中斷狀態;
    4. 執行緒如果被中斷了,中斷當前執行緒,被中斷的執行緒還是會繼續執行後面邏輯
    • 以上過程涉及到的技術點有:CAS,自旋,佇列入隊,佇列刪除節點(被取消的節點),阻塞執行緒(LockSupport.park(this))

釋放鎖:public final boolean release(int arg)

unlock方法呼叫的是sync.release(1),而release是AQS 方法中的方法,表示將同步狀態設定回初始狀態,將鎖釋放。

public final boolean release(int arg) {
    // tryRelease 是我們自己的實現,就是把state欄位設定成0,如果是可重入的,只能慢慢減到初始狀態
	  if (tryRelease(arg)) {
        // 進入到這裡說明CAS 設定成功,也就代表鎖成功釋放了,需要喚醒佇列中的第一個排隊的節點執行緒
	      Node h = head;
        // head 表示的是當前獲得鎖的節點
	      if (h != null && h.waitStatus != 0)
            // 喚醒頭結點的下一個節點
	          unparkSuccessor(h);
	      return true;
	  }
	  return false;
}
private void unparkSuccessor(Node node) {
    // 這裡的node 是當前持有鎖的節點
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);     
    // 找到頭結點的後繼節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
         // 大於0的狀態只有1,表示被取消了,如果被取消了,就繼續取下一個節點喚醒
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        **LockSupport.unpark(s.thread);**
}

釋放鎖的邏輯比加鎖邏輯要簡單很多,主要邏輯有:

  1. 修改同步狀態為初始值,方法tryRelease(arg)

    這裡我們自己實現的會直接將同步狀態設定為0,如果是支援可重入,就需要慢慢減了;

  2. 釋放鎖儲層,喚醒頭結點(當前獲得鎖的節點)的後繼節點:unparkSuccessor(h);
    找到頭結點的後繼節點中第一個沒有被取消的節點,並喚醒該節點所處執行緒

基於AQS實現自己的共享鎖

設計一個同步器,在同一時刻,只允許最多兩個執行緒能夠並行訪問,超過限制的其他執行緒將進入阻塞狀態。

這個功能和 Semaphore 的功能很相似,這個學會了,以後看 Semaphore 的原始碼也就很簡單了。

實現思路:

可以利用AQS 的API tryAcquireShared 實現獲得共享鎖,定義一個狀態,允許的範圍為【0,1,2】,狀態為2代表新的執行緒進入的時候需要阻塞等待

public class MyAqsSharedLock{

    // 定義最大共享值
    private final int maxSharedValue = 2;
    // 同步器
    private final Sync sync;

    MyAqsSharedLock(){
        // 構造方法初始化同步器
        sync = new Sync();
    }
    // 基於AQS實現的同步器
    class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected int tryAcquireShared(int arg){
            // 為什麼要自旋呢?因為可能滿足state的條件,但是CAS修改失敗
            while(true){
                int state = getState();
                // 檢查同步狀態是否達到最大值
                if(state >= maxSharedValue){
                    // 返回-1 表示沒有獲得鎖
                    return -1;
                }
                // CAS 修改同步狀態
                if(compareAndSetState(state,state + arg)){
                    // 修改成功,表示獲得了鎖,大於等於0表示獲得了鎖
                    return getState();
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg){
            // 為什麼要自旋呢?因為可能滿足state的條件,但是CAS修改失敗
            while(true){
                int state = getState();
                // CAS 修改同步狀態,修改成功返回true,失敗繼續自旋 
                if(compareAndSetState(state,state - arg)){
                    return true;
                }
            }
        }
    }
    /** 加鎖 */
    public void lock(){
        sync.acquireShared(1);
    }
    /** 解鎖 */
    public void unLock(){
        sync.releaseShared(1);
    }
}

測試方法:

5個執行緒迴圈列印輸出執行緒名和當前時間

public static void main(String[] args){
        MyAqsSharedLock lock = new MyAqsSharedLock();
        IntStream.range(0,5).forEach(i -> new Thread(new Runnable(){
            @SneakyThrows
            @Override
            public void run(){
                while(true){
                    lock.lock();
                    try{
                        System.out.println(Thread.currentThread().getName()+":執行。。。時間:"+ LocalDateTime.now());
                        TimeUnit.SECONDS.sleep(2);
                    }finally{
                        lock.unLock();
                        TimeUnit.SECONDS.sleep(1);
                    }

                }
            }
        },"T"+i).start());
    }

輸出結果示例:

T0:執行。。。時間:2020-10-30T17:45:45.117
T1:執行。。。時間:2020-10-30T17:45:45.117
T3:執行。。。時間:2020-10-30T17:45:47.118
T2:執行。。。時間:2020-10-30T17:45:47.118
T1:執行。。。時間:2020-10-30T17:45:49.119
T4:執行。。。時間:2020-10-30T17:45:49.119

會發現幾乎在同一時間最多隻有2個執行緒在列印輸出,滿足我們的要求。

程式碼實現分析

獲得共享鎖:public void lock()

共享鎖的 lock() 呼叫的是sync.acquireShared(1)acquireShared也在AQS裡面,同樣被final修飾作為基礎框架邏輯部分,不允許被繼承,原始碼展示:

public final void acquireShared(int arg) {
    // tryAcquireShared 是我們自己實現的邏輯,返回-1,表示沒有獲得鎖
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
// 沒有獲得共享,再次嘗試獲得鎖,和排他模式的acquireQueued方法非常相似
private void doAcquireShared(int arg) {
    // 新建節點,加入到佇列,和排他鎖模式一樣的入隊邏輯
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 當前節點的前驅節點
            final Node p = node.predecessor();
            if (p == head) {
                // 前驅節點是頭結點,說明輪到我們獲得鎖了
                // 繼續呼叫我們自己的邏輯,CAS 獲得鎖
                int r = tryAcquireShared(arg);
                // 這裡再次印證了,我們的tryAcquireShared返回值定義,負值是沒有獲得鎖,>=0 表示成功獲得鎖
                if (r >= 0) {
                    // 設定新的頭結點,如果後面的排隊節點是共享模式的節點,直接喚醒它
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        // 中斷當前執行緒
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 到這了,說明要麼沒有排隊到當前執行緒,要麼CAS獲取鎖失敗,那就只有阻塞執行緒了
            // shouldParkAfterFailedAcquire 如果執行緒應阻塞,則返回true
            // parkAndCheckInterrupt  阻塞當前執行緒admol, 具體實現分析可以看上面lock的分析
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 執行緒被取消,摘掉節點
        if (failed)
            cancelAcquire(node);
    }
}

獲取共享鎖總結:

  1. 嘗試獲得鎖,方法:tryAcquireShared
    1. 獲得鎖成功,直接返回;獲得鎖失敗,繼續執行下面邏輯;
  2. 再次嘗試獲得鎖,方法:doAcquireShared
    1. 新建排隊節點,並加入到同步佇列,方法:addWaiter,邏輯和獲得排它鎖的一致
    2. 自旋(嘗試獲得鎖,阻塞執行緒,等待被喚醒),直到成功獲得鎖

釋放共享鎖:public void unLock()

unlock方法呼叫的是sync.releaseShared(1),releaseShared也是AQS 方法中的方法,不允許被繼承,表示將同步狀態設定回初始狀態,將鎖釋放。

public final boolean releaseShared(int arg) {
    // tryReleaseShared 我們自己實現的邏輯
    if (tryReleaseShared(arg)) {
        // 釋放鎖失敗,繼續釋放,自旋直到釋放成功
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    // 自旋
    for (;;) {
        Node h = head;
        // 同步等待的佇列不為空
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 檢查狀態是否要喚醒下一個節點的執行緒
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 加入h節點是持有鎖的節點,會喚醒它的下一個節點執行緒
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 理論上喚醒一個就會退出
        if (h == head)                   // loop if head changed
            break;
    }
}

總結

  1. AQS是Java中可以實現同步器功能的一個基礎框架,我們自己也可以基於AQS實現想要的同步功能
  2. AQS 中用Node節點維護了一個雙向連結串列,用來儲存排隊獲取鎖的執行緒,已經用來喚醒執行緒
  3. AQS 中為了一個state的同步狀態變數,可以基於這個變數實現很多功能
  4. 實現AQS的幾個重要API,就可以實現一個簡單同步器的功能,其他像自旋,排隊,阻塞,喚醒,AQS都已經幫我們做好了

相關文章