鎖:AbstractQueuedSynchronizer源碼解析(上)


AbstractQueuedSynchronizer是同步器,簡稱AQS,是各類線程鎖的基礎。該類的方法很多,針對包裝線程的節點的操作也很多。

1.整體架構

AQS整體處理流程如下,
image
對上圖進行說明,

  1. AQS中的隊列有兩種,同步隊列和條件隊列,其底層數據結構是鏈表
  2. 四種顏色的線條代表不同的場景

AQS本身就是一套鎖的框架,定義了獲得鎖和釋放鎖的代碼,所以繼承AQS抽象類並實現相應的方法即可實現鎖。

類註釋

AQS的類註釋中包含的信息如下,

  1. AQS提供了一個框架,定義了先進先出的同步隊列,讓獲取不到鎖的線程在同步隊列中排序
  2. 同步器存在一個成員變量status,表示同步器的狀態,用於判斷AQS是否能夠得到鎖。該變量使用volatile關鍵字進行修飾保證其線程安全
  3. AQS的子類可以通過CAS的方式給status賦值,定義哪些狀態可以獲取鎖,哪些狀態獲取不到鎖
  4. AQS提供兩種鎖模式,共享鎖和排它鎖。
    i) 排他模式:只有一個線程可以獲得鎖
    ii)共享模式:多個線程可以同時獲得鎖
    AQS的子類 ReadWriteLock實現了這兩種模式
  5. AQS的內部類ConditionObject是條件隊列的實現類。通過new Condition()可以創建一個條件隊列。鎖對象中,條件隊列的數目可以是多個
  6. AQS繼承了 AbstractOwnableSynchronizer類,該類可以追蹤獲得鎖的線程

類定義

AQS的類定義如下,

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {}
  1. AQS是抽象類,該類中只實現類將線程放入和取出同步隊列、條件隊列的方法,定義瞭如何獲得鎖、如何釋放鎖的抽象方法,目的是讓子類去實現爭鎖和釋放鎖的過程
  2. 繼承了AbstractOwnableSynchronizer類,該類的作用就是爲了知道哪個線程獲取了鎖,便於管理

類屬性

1)一般屬性

// 同步器狀態,子類會根據狀態字段進行判斷是否可以獲得鎖或釋放鎖
// CAS給該變量賦值,獲得鎖+1,釋放鎖-1
private volatile int state;

// 自旋超時閥值,單位納秒。當設置等待時間時纔會用到這個屬性
static final long spinForTimeoutThreshold = 1000L;

2)同步隊列屬性

同步隊列的說明如下,

  1. 當多個線程都來請求鎖時,某一時刻有且只有一個線程能夠獲得鎖(排它鎖)。其餘獲取不到鎖的線程,都會到同步隊列中去排隊並阻塞自己。
  2. 當有線程主動釋放鎖時,就會從同步隊列頭開始釋放一個排隊的線程,讓線程重新去競爭鎖。
  3. 同步隊列的底層是雙向鏈表
private transient volatile Node head;

private transient volatile Node tail;

同步隊列的頭和尾,是AQS的屬性。

3) 條件隊列屬性

條件隊列的說明如下,

  1. 條件隊列同樣用於管理獲取不到鎖的線程
  2. 條件隊列的底層是單向鏈表(與同步隊列區分)
  3. 條件隊列不直接和鎖打交道
public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    // 條件隊列中第一個 node
    private transient Node firstWaiter;
    // 條件隊列中最後一個 node
    private transient Node lastWaiter;
}  

條件隊列實現的是 Condition接口。

4)Node

Node既是同步隊列的節點,同時也是條件隊列的節點。該類用於包裝線程,

static final class Node {
    // 同步隊列單獨的屬性
    //node共享模式或排他模式
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;

    // 當前節點的前節點
    // 節點 acquire 成功後就會變成head
    // head 節點不能被 cancelled
    volatile Node prev;

    // 當前節點的下一個節點
    volatile Node next;

    //  兩個隊列共享的屬性
    // 表示當前節點的狀態,通過節點的狀態來控制節點的行爲
    // 普通同步節點,就是 0 ,條件節點是 CONDITION -2
    volatile int waitStatus;
    // waitStatus 的狀態有以下幾種
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;

    // 當前節點包裝的線程
    volatile Thread thread;

    // 在同步隊列中,nextWaiter並不真的是指向其下一個節點,只是表示當前 Node 是排它模式還是共享模式
    // 但在條件隊列中,nextWaiter 就是表示下一個節點元素
    Node nextWaiter;
}

Node類中的waitStatus一定與AQS的status進行區分,Node的操作是通過waitStatus決定的。

對Node節點的狀態進行如下說明,

狀態 說明
CANCELLED 被取消
SIGNAL 同步隊列中的節點在自旋獲取鎖的時候, 如果前一個節點的狀態是 SIGNAL,那麼自己就可以阻塞休息了;否則自己一直自旋嘗試獲得鎖
CONDITION 表示當前 node 正在條件隊列中。當節點從同步隊列轉移到條件隊列時,狀態就會被更改成 CONDITION
PROPAGATE 無條件傳播,共享鎖模式下,該狀態的進程處於可運行狀態

5)共享鎖和排它鎖的區別

同一時刻只能有一個線程可以獲得排它鎖,也只能有一個線程可以釋放鎖。

共享鎖同一時刻允許多個線程獲得同一個鎖,並可以設置獲取鎖的線程的數目。

Condition接口

條件隊列ConditionObject實現了Condition接口,

1)類註釋

  1. 當使用鎖對象替代 synchronize時,Condition用於替代 Object中的監控方法,如Object#wait()Object#notify()Object#notifyAll()等方法
  2. 線程被暫停執行後,等待其他線程將其喚醒
  3. Condition實例綁定在鎖對象上,通過Lock#newCondition()方法可以創建鎖的條件隊列

2)條件隊列示例

假設存在一個有邊界的隊列,支持 put和 take方法存入或取出元素,

  1. 如果試圖向空隊列執行take操作,線程將會阻塞,直到隊列中有可用的元素爲止
  2. 如果試圖向滿隊列執行put操作,線程將會阻塞,直到隊列中有空閒的位置爲止

上面的例子中,如果兩個操作依靠一個條件隊列,那麼每次只能執行其中一個操作。所以可以創建兩個條件隊列,分別執行存入和取出的操作。

3)接口方法

Condition接口定義了一些方法,這些方法奠定了條件隊列的基礎,

void await() throws InterruptedException;

該方法使當前線程一直等待,直到被signalsignalAll方法喚醒。

條件隊列中的線程被喚醒的四種情況,

  1. 有線程使用了signal方法,喚醒了條件隊列中的當前線程。該方法喚醒條件隊列中的一個線程,在被喚醒前必須先獲得鎖
  2. 有線程使用了signalAll方法,該方法喚醒條件隊列中的所有線程
  3. 其他線程打斷了當前線程
  4. 虛假喚醒

線程從條件隊列中甦醒時,必須重新獲得鎖,才能真正被喚醒

2.同步器狀態

AQS中存在兩個狀態,statuswaitStatus,二者一定要區分,

  • status是鎖的狀態,是 int 類型。子類繼承 AQS 時,都是要根據 state 字段來判斷有無得到鎖,比如當前同步器狀態是 0,表示可以獲得鎖,當前同步器狀態是 1,表示鎖已經被其他線程持有,當前線程無法獲得鎖;
  • waitStatus 是節點(Node)的狀態,種類很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3),各個狀態的含義可以見上文。

3.獲取鎖

在AQS的子類中通常使用Lock#lock()方法獲得鎖,使得線程能夠取得資源的使用權限。Lock是AQS的子類,lock方法或根據情況調用AQS的acquiretryAcquire方法。

acquire 方法 AQS 已經實現了,tryAcquire 方法是等待子類去實現。

  • acquire 方法制定了獲取鎖的框架,先嚐試使用 tryAcquire 方法獲取鎖,獲取不到時,再入同步隊列中等待鎖
  • tryAcquire 方法在 AQS 中直接拋出一個異常,表明需要子類去實現,子類可以根據同步器的 state 狀態來決定是否能夠獲得鎖

獲得排它鎖—acquire

public final void acquire(int arg) {
    // tryAcquire 方法是需要實現類去實現的,實現思路一般都是 cas 給 state 賦值來決定是否能獲得鎖
    if (!tryAcquire(arg) &&
        // addWaiter 入參代表是排他模式
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

該流程對應架構圖的紅線部分,
在這裏插入圖片描述
具體流程爲,

  1. 嘗試執行一次tryAcquire方法,如果成功則線程執行任務即可。如果失敗則走下面的流程進入等待隊列
  2. 調用addWaiter方法將線程包裝成Node,添加到同步隊列尾部。addWaiter方法返回包裝了當前線程的Node對象
  3. 調用acquireQueued方法,該方法的作用有兩個,
    i. 阻塞當前線程
    ii. 節點被喚醒時,使其能夠獲得鎖
    這兩個功能的實現是依靠acquireQueued方法中自旋過程
  4. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 如果返回值爲true說明線程是在同步隊列中阻塞一段時間後才被取出,此時線程的interrupt狀態爲true,調用selfInterrupt()interrupt改爲false

1)addWaiter方法

用於將未獲取到鎖的線程添加到同步隊列尾部,

private Node addWaiter(Node mode) {
    // 初始化包裝當前線程的Node對象
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    // 如果同步隊列不爲空,先簡單嘗試將節點加到隊列尾部,通常情況下會成功加入
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果隊列爲空或簡單嘗試失敗則自旋保證node加入到隊尾
    enq(node);
    return node;
}

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 如果隊尾爲空,說明當前同步隊列都沒有初始化,進行初始化
        // tail = head = new Node();
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // 隊尾不爲空,將當前節點追加到隊尾
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在 addWaiter 方法中,並沒有進入方法後立馬就自旋,而是先嚐試一次追加到隊尾,如果失敗才自旋,因爲大部分操作可能一次就會成功,這種思路在寫自旋的時候可以借鑑。

2)acquireQueued方法

將線程加入到同步隊列尾部後,需要使當前線程阻塞,acquireQueued方法用於實現這一功能,

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            final Node p = node.predecessor();	// 獲取當前線程節點的前一個節點
            // 有兩種情況會走到 p == head,見下方說明
            if (p == head && tryAcquire(arg)) {
                // 成功獲得鎖,則將自身設置成head節點並回收其前置節點
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }

            // shouldParkAfterFailedAcquire和parkAndCheckInterrupt 用於阻塞當前線程
            // 線程是在這個方法裏面阻塞的,醒來的時候仍然在for循環裏面,就能再次自旋嘗試獲得鎖
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        // 如果獲得node的鎖失敗,將 node 從隊列中移除
        if (failed)
            cancelAcquire(node);
    }
}

方法的自旋過程在每一次循環開始時都會有一步判斷p == head,滿足這一條件通常有兩種情況,

  1. node 之前沒有獲得鎖,addWaiter方法中調用enq方法初始化同步隊列後將其添加到了新創建的head節點之後。
    此時進入 acquireQueued 方法時,才發現當前節點的前置節點就是頭節點,於是嘗試獲得一次鎖
  2. node節點之前一直在阻塞沉睡,然後被喚醒(喚醒操作從head節點開始)。此時喚醒 node 的節點正是其前一個節點。

如果 tryAcquire 成功,就立馬把自己設置成 head,把其前置節點移除(因爲前置節點是之前的head,同步隊列的頭節點相當於一個沒有用的空節點)。
如果 tryAcquire 失敗,嘗試進入同步隊列。

acquireQueued方法的返回值進行說明,

  1. 返回true,說明節點是進入到同步隊列之後阻塞了一段時間才被從隊列中取出
  2. 返回false,說明節點進入了同步隊列,但是沒等到被阻塞就被從隊列中取出,即沒有執行到shouldParkAfterFailedAcquireparkAndCheckInterrupt方法

setHead方法

將節點設置爲同步隊列的頭節點,

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

同步隊列的頭節點相當於一個空節點,其作用只是指向其下一個節點。

shouldParkAfterFailedAcquire方法

該方法用於將當前的Node對象的前置節點waitStatus設置爲SIGNAL前置節點的狀態爲SIGNAL時,當前節點就可以阻塞

// 參數是當前節點的前置節點和當前節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前置節點狀態已經是SIGNALED,說明當前節點不是新入隊的節點
    // 該線程可以安全的被park阻塞
    if (ws == Node.SIGNAL)	
        return true;
    // 前置節點是CANCELLED狀態說明該節點無效,將當前節點掛到更前面的有效節點之後    
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {	
    // 前置節點狀態是0或PROPAGATE說明當前節點是新入隊的節點
    // 需要將前置節點的狀態改爲SIGNAL,但是不立刻對其執行park操作。而是返回false,回到自旋過程中在新一輪循環中才阻塞
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire方法返回值的說明,

  • 返回 true,前置節點狀態已經是SIGNALED,當前節點的線程可以被安全阻塞
  • 返回 false,前置節點狀態剛剛被調整爲SIGNALED,當前節點的線程不立刻阻塞。而是回到acquireQueued方法的自旋過程中在新一輪循環中阻塞

parkAndCheckInterrupt方法

該方法用於阻塞線程,線程是在這個方法裏面阻塞的,醒來的時候仍然在acquireQueued方法的 for 循環裏面,就能再次自旋嘗試獲得鎖,

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

獲取共享鎖—acquireShared方法

acquireShared 整體流程和 acquire 相同,代碼也很相似,貼出來不一樣的代碼進行比較,

1)嘗試獲取鎖—tryAcquireShared方法

image

2)setHeadAndPropagate方法

第二步不同,在於節點獲得排它鎖時,僅僅把自己設置爲同步隊列的頭節點即可(setHead 方法)。但如果是共享鎖的話,還會去喚醒自己的後續節點,一起來獲得該鎖(setHeadAndPropagate 方法)。

不同之處如下(左邊排它鎖,右邊共享鎖),
image
setHeadAndPropagate方法源碼如下,

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    setHead(node);
    // propagate > 0 表示已經有節點獲得共享鎖了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //共享模式,還喚醒頭節點的後置節點
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

// 釋放後置共享節點
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 還沒有到隊尾,此時隊列中至少有兩個節點
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 如果隊列狀態是 SIGNAL ,說明後續節點都需要喚醒
            if (ws == Node.SIGNAL) {
                // CAS 保證只有一個節點可以運行喚醒的操作
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                // 進行喚醒操作
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                
        }
        
        if (h == head)   
            break;
    }
}

總結

本節的重點是獲取排它鎖的acquire方法,整體的流程圖如下,
在這裏插入圖片描述

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