天天看點

AQS解鎖原理ReectrantLock 和 AQS 是什麼關系ReectrantLock 标準使用方式解鎖過程總結

ReectrantLock 和 AQS 是什麼關系

ReectrantLock 是在 AQS 外面包了一層,ReectrantLock 所有的加鎖、解鎖、打斷、取消等等,底層都是交給 AQS完成的。

ReectrantLock 标準使用方式

class X {
   private final ReentrantLock lock = new ReentrantLock();
   // ...

   public void m() {
     // 加鎖
     lock.lock();  // block until condition holds
     try {
        // 被加鎖的代碼塊
       // ... method body
     } finally {
       // 解鎖
       lock.unlock()
     }
   }
 
           

解鎖過程

public void unlock() {
    // 調用非公平鎖的釋放鎖方法。 
    sync.release(1);
}
           
public final boolean release(int arg) {
    // 釋放鎖,
    if (tryRelease(arg)) {
        // 拿頭結點。
        Node h = head;
        // t頭結點不是空,而且頭結點的 waitStatus 此時是 -1。
        if (h != null && h.waitStatus != 0)
            // 喚醒離頭結點最近的沒有被取消的節點。
            // 這裡是傳了頭結點進去。
            unparkSuccessor(h);
        return true;
    }
    return false;
}
           

釋放鎖的細節:

protected final boolean tryRelease(int releases) {
    // 這跟鎖重入有關系,當 c == 0 時,才能釋放成功。
    // 比如,T0 加鎖四次,getState() == 4,釋放三次 releases ==3
    // c=4-3, T0 當然不能釋放成功,還有一層鎖呢。
    int c = getState() - releases;
    // 如果目前線程不是鎖的持有者,抛異常。
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 當 c==0 時,鎖才能釋放成功。
    if (c == 0) {
        free = true;
        // 将同步器中鎖的持有者置為 null。
        setExclusiveOwnerThread(null);
    }
    // CAS 修改 state = 0。
    setState(c);
    // 傳回
    return free;
}
           

喚醒離頭結點最近的沒有被取消的節點,在頭結點後面 可能有節點(線程)已經被取消了,那就将這些被取消的線程從隊列中移除,這個樣的話,第一個沒有被取消的節點就放在了頭結點後面,也就是隊列中的第二個節點的位置。

// node == head
private void unparkSuccessor(Node node) {
    // ws == -1
    int ws = node.waitStatus;
    if (ws < 0)
        // 設定隊頭節點的 waitStatus == 0
        compareAndSetWaitStatus(node, ws, 0);    
    // s 先指向隊列中的第二個節點。
    Node s = node.next;
    // 如果第二個節點是空,或者被取消了。
    if (s == null || s.waitStatus > 0) {
        s = null;
        // t 從隊尾開始,朝着隊頭的方向,掃一遍隊列中的所有節點。
        for (Node t = tail; t != null && t != node; t = t.prev)
            // s 指向離隊頭最近的且沒有被取消的線程節點。
            if (t.waitStatus <= 0)
                s = t;
    }
    // unpark s 指向的節點中的線程。
    if (s != null)
        LockSupport.unpark(s.thread);
}
           

喚醒 node 後,node還要繼續去競争鎖。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 死循環
        for (;;) {
            // 被喚醒後又一次循環, 此時被喚醒的節點是隊列中的第二個節點。
            // 擷取前驅。
            final Node p = node.predecessor();
            // 前驅一定是頭結點。競争鎖
            if (p == head && tryAcquire(arg)) {
                // 競争成功,出隊執行線程邏輯。
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 競争失敗,繼續追加到隊列裡 park。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 從這裡喚醒的。
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

           

被喚醒變了怎麼還要去競争鎖,而且還有失敗的可能性?

因為這是非公平鎖,既有被喚醒的,可能同時還有剛産生的線程,兩者會競争鎖。

總結

釋放鎖過程做三件事情:

  1. 釋放鎖
  2. 喚醒離頭結點最近的第一個沒有被取消的 node。
  3. 被喚醒的 node 再去競争鎖,成功,node出隊執行邏輯代碼,失敗,繼續追加到隊列裡 park。

繼續閱讀