一文帶你學會AQS和併發工具類的關係2

雪中孤狼發表於2021-01-18

1.建立公平鎖

1.使用方式

Lock reentrantLock = new ReentrantLock(true);
reentrantLock.lock(); //加鎖
try{
  // todo
} finally{
  reentrantLock.unlock(); // 釋放鎖
}

2.建立公平鎖

在new ReentrantLock(true)的時候加入關鍵字true

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

當傳入的引數值為true的時候建立的物件為new FairSync()公平鎖。

2.加鎖的實現

1.普通的獲取鎖

reentrantLock.lock(); //加鎖

加鎖的實際呼叫的方法是建立的公平鎖裡面的lock方法
圖片

static final class FairSync extends Sync {
    final void lock() {
        acquire(1);
    }
    ...
}

程式碼中的acquire方法和非公平鎖中的acquire方法一樣都是呼叫的AQS中的final方法

## AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

不過不同之處是這裡面的tryAcquire(arg)方法是呼叫的公平鎖裡面實現的方法
圖片

這個方法其實和非公平鎖方法特別相似,只有一處不同公平鎖中含有一個特殊的方法叫做hasQueuedPredecessors()該方法也是AQS中的方法,該方法的實質就是要判斷該節點的前驅節點是否是head節點

## AbstractQueuedSynchronizer
public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

剩下的部分和前一篇分析的非公平鎖幾乎是一個流程

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 設定當前執行緒為當前鎖的獨佔執行緒
            setExclusiveOwnerThread(current);
            // 獲取鎖成功
            return true;
        }
    }
    // 如果是當前執行緒持有的鎖資訊,在原來的state的值上加上acquires的值
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 設定state的值
        setState(nextc);
        // 獲取鎖成功
        return true;
    }
    // 獲取鎖失敗了才返回false
    return false;
}

注意一下只有當返回false的時候才是tryAcquire失敗的時候。此時就會走到繁瑣的addWaiter(Node.EXCLUSIVE)方法

2.普通獲取鎖失敗

如果前面tryAcquire失敗就會進行接下來的addWaiter(Node.EXCLUSIVE)

## AbstractQueuedSynchronizer
private Node addWaiter(Node mode) {
    // 建立一個新的node節點 mode 為Node.EXCLUSIVE = null
    Node node = new Node(Thread.currentThread(), mode);
    // 獲取尾部節點
    Node pred = tail;
    // 如果尾部節點不為空的話將新加入的節點設定成尾節點並返回當前node節點
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果尾部節點為空整明當前佇列是空值得需要將當前節點入隊的時候先初始化佇列
    enq(node);
    return node;
}

3.節點入隊方法

enq(node)方法是節點入隊的方法我們來分析一下,enq入隊方法也是AQS中的方法,注意該方法的死迴圈,無論如何也要將該節點加入到佇列中。

## AbstractQueuedSynchronizer
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果尾節點為空的話,那麼需要插入一個新的節點當頭節點
        if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
    // 如果不為空的話,將當前節點變為尾節點並返回當前節點的前驅節點
        node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

其實和非公平鎖的addWaiter(Node node)是一樣的流程,分析完。

4.acquireQueued方法

此時當前節點已經被加入到了阻塞佇列中了,進入到了acquireQueued方法。該方法也是AQS中的方法。

## AbstractQueuedSynchronizer
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;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

注意 setHead(node) 中具體的實現細節thread為null,prev也為null其實就是如果當前節點的前驅節點為頭節點的話,那麼當前節點變成了頭節點也就是之前阻塞佇列的虛擬頭節點。

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

如果不是頭節點或者tryAcquire()方法執行失敗執行下面的更加繁瑣的方法shouldParkAfterFailedAcquire(p, node),如果該方法返回true才會執行到下面的parkAndCheckInterrupt()方法,這兩個方法都是AQS中的方法。

## AbstractQueuedSynchronizer
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前驅節點的狀態
int ws = pred.waitStatus;
// 如果前驅節點的狀態為SIGNAL那麼直接就可以沉睡了,因為如果一個節點要是進入
    // 阻塞佇列的話,那麼他的前驅節點的waitStatus必須是SIGNAL狀態。
    if (ws == Node.SIGNAL)
return true;
// 如果前驅節點不是Node.SIGNAL狀態就往前遍歷一值尋找節點的waitStatus必須
    // 是SIGNAL狀態的節點
    if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
    // 如果沒有找到符合條件的節點,那麼就將當前節點的前驅節點的waitStatus
        // 設定成SIGNAL狀態
    compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

如果返回的值是false,就要注意此時又繼續進入了下一次死迴圈中,因為如果往前遍歷的過程中有可能他的前驅節點變成了頭節點,那麼就可以再次的獲取鎖,如果不是的話那麼只能
執行parkAndCheckInterrupt()方法進行執行緒的掛起了。

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

5.取消請求

無論如何最終都走到了cancelAcquire方法

private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
Node pred = node.prev;
// 跳過所有取消請求的節點
    while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 將當前節點設定成取消狀態,為了後續遍歷跳過我們
    node.waitStatus = Node.CANCELLED;
// 如果當前節點是尾節點,並且將當前節點的前驅節點設定成尾節點成功
    if (node == tail && compareAndSetTail(node, pred)) {
    // 當前節點的前驅節點的後續節點為空
    compareAndSetNext(pred, predNext, null);
} else {
    int ws;
// 如果前驅節點不是頭節點
        if (pred != head &&
        // 前驅節點的狀態是Node.SIGNAL或者前驅節點的waitStatus設定           
        // 成Node.SIGNAL
        ((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
// 前驅節點的thread 不為空
            pred.thread != null) {
// 獲取當前節點的後繼節點
            Node next = node.next;
// 如果後繼節點不為空,並且後繼節點waitStatus 小於0
            if (next != null && next.waitStatus <= 0)
// 將當前節點的後繼節點設定成當前節點的前驅節點的後繼節點
            compareAndSetNext(pred, predNext, next);
} else {
// 如果上面當前節點的前驅節點是head或者其他條件不滿足那麼就喚醒當前節點
        unparkSuccessor(node);
}
node.next = node; // help GC
}
}

unparkSuccessor(node)喚醒當前節點,該方法也是AbstractQueuedSynchronizer中的方法

private void unparkSuccessor(Node node) {
    // 獲取當前節點的狀態
    int ws = node.waitStatus;
    // 如果當前節點狀態小於0那麼設定成0
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 獲取當前節點的後繼節點
    Node s = node.next;
    // 如果後繼節點為空,或者後繼節點的狀態小於0
    if (s == null || s.waitStatus > 0) {
        // 後繼節點置為null。視為取消請求的節點
        s = null;
        // 獲取尾節點,並且尾節點不為空,不是當前節點,那麼就往前遍歷尋找
        // 節點waitStatus 狀態小於0的節點賦予給當前節點的後繼節點
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
     // 喚醒後繼節點
        LockSupport.unpark(s.thread);
}

3.獲取鎖的流程圖

流程圖和上一篇非公平鎖的獲取流程圖十分相似只有一點點區別這裡就不過多的描述了。

4.釋放鎖的實現

4.1釋放鎖程式碼分析

嘗試釋放此鎖。如果當前執行緒是此鎖的持有者,則保留計數將減少。 如果保持計數現在為零,則釋放鎖定。 如果當前執行緒不是此鎖的持有者,則丟擲IllegalMonitorStateException。

## ReentrantLock
public void unlock() {
    sync.release(1);
}

sync.release(1) 呼叫的是AbstractQueuedSynchronizer中的release方法

## AbstractQueuedSynchronizer
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

分析tryRelease(arg)方法
圖片

tryRelease(arg)該方法呼叫的是ReentrantLock中

protected final boolean tryRelease(int releases) {
// 獲取當前鎖持有的執行緒數量和需要釋放的值進行相減
    int c = getState() - releases; 
    // 如果當前執行緒不是鎖佔有的執行緒丟擲異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果此時c = 0就意味著state = 0,當前鎖沒有被任意執行緒佔有
    // 將當前所的佔有執行緒設定為空
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 設定state的值為 0
    setState(c);
    return free;
}

如果頭節點不為空,並且waitStatus != 0,喚醒後續節點如果存在的話。
這裡的判斷條件為什麼是h != null && h.waitStatus != 0?

因為h == null的話,Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛擬節點。所以說,這裡如果還沒來得及入隊,就會出現head == null 的情況。

  1. h != null && waitStatus == 0 表明後繼節點對應的執行緒仍在執行中,不需要喚醒
  2. h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒
private void unparkSuccessor(Node node) {
// 獲取頭結點waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
// 獲取當前節點的下一個節點
    Node s = node.next;
//如果下個節點是null或者下個節點被cancelled,就找到佇列最開始的非cancelled的節點
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 就從尾部節點開始找往前遍歷,找到佇列中第一個waitStatus<0的節點。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
  // 如果當前節點的下個節點不為空,而且狀態<=0,就把當前節點喚醒
    if (s != null)
        LockSupport.unpark(s.thread);
}

為什麼要從後往前找第一個非Cancelled的節點呢?
看一下addWaiter方法

private Node addWaiter(Node mode) {
    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;
        }
    }
    enq(node);
    return node;
}

我們從這裡可以看到,節點入隊並不是原子操作,也就是說,node.prev = pred, compareAndSetTail(pred, node) 這兩個地方可以看作Tail入隊的原子操作,但是此時pred.next = node;還沒執行,如果這個時候執行了unparkSuccessor方法,就沒辦法從前往後找了,所以需要從後往前找。還有一點原因,在產生CANCELLED狀態節點的時候,先斷開的是Next指標,Prev指標並未斷開,因此也是必須要從後往前遍歷才能夠遍歷完全部的Node
所以,如果是從前往後找,由於極端情況下入隊的非原子操作和CANCELLED節點產生過程中斷開Next指標的操作,可能會導致無法遍歷所有的節點。所以,喚醒對應的執行緒後,對應的執行緒就會繼續往下執行。

4.2 釋放鎖流程圖

圖片

5.注意

下一篇講解併發工具包下的LockSupport,謝謝大家的關注和支援!有問題希望大家指出,共同進步!!!

相關文章