【JavaSE】Lock鎖,獨佔鎖ReentrantLock的AQS原始碼,如何管理同步佇列。acquire方法和release方法

馮某r發表於2019-01-23

一、Lock鎖具體是如何實現的

由上一篇部落格解釋,實現Lock鎖的子類,實現了介面的所有方法。每個方法又都依賴Sync這個內部靜態類來實現的,所以主要看一下Sync這個內部靜態類。

abstract static class Sync extends AbstractQueuedSynchronizer

Sync繼承了AbstractQueuedSynchronizer這個抽象類,其實它是java語言中一個重要的佇列同步器,簡稱AQS。它是構建鎖或者其他同步元件的基礎框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等)。
下面先來看一下AQS中和Lock鎖有關的一些方法:
獨佔鎖(ReentrantLock)本篇講解獨佔鎖

void acquire(int arg) //獨佔式獲取同步狀態,如果獲取失敗則插入同步佇列進行等待。 
void acquireInterruptibly(int arg) //與acquire方法相同,但在同步佇列中等待時可以響應中斷。 
boolean tryAcquireNanos(int arg,long nanosTimeout) //在2的基礎上增加了超時等待功能,在超時時間內沒有獲 得同步狀態返回false 
boolean tryAcquire(int arg) //獲取鎖成功返回true,否則返回false 
boolean release(int arg) //釋放同步狀態,該方法會喚醒在同步佇列中的下一個節點。

共享鎖(ReentrantReadWriteLocK)

void acquireShared(int arg) //共享式獲取同步狀態,與獨佔鎖的區別在於同一時刻有多個執行緒獲取同步狀態。 
void acquireSharedInterruptibly(int arg) //增加了響應中斷的功能 
boolean tryAcquireSharedNanos(int arg,lone nanosTimeout) //在2的基礎上增加了超時等待功能 
boolean releaseShared(int arg) //共享鎖釋放同步狀態。

上面程式碼的解析中提到了一個同步佇列,這個佇列就是來管理那些同時競爭一個鎖的時候,沒有競爭到鎖的執行緒,會進行排隊放在一個資料結構中進行管理。那麼這個資料結構是怎樣的形式存在的?
在AQS有一個靜態內部類Node,這是我們同步佇列的每個具體節點。在這個類中有如下屬性

volatile int waitStatus; // 節點狀態 
volatile Node prev; // 當前節點的前驅節點 
volatile Node next; // 當前節點的後繼節點 
volatile Thread thread; // 當前節點所包裝的執行緒物件 
Node nextWaiter; // 等待佇列中的下一個節點

可以初步推斷出這些沒有競爭到鎖的執行緒,會被封裝成一個節點,然後以雙向連結串列的形式管理起來。
節點狀態是由一個簡單的int型來儲存的,用來表示此執行緒目前所處的狀態:

int INITIAL = 0; // 初始狀態 
int CANCELLED = 1; // 當前節點從同步佇列中取消 
int SIGNAL = -1; // 後繼節點的執行緒處於等待狀態,如果當前節點釋放同步狀態會通知後繼節點,使得後繼 節點的執行緒繼續執行。 
int CONDITION = -2; // 節點在等待佇列中,節點執行緒等待在Condition上,當其他執行緒對Condition呼叫了 signal()方法後,該節點將會從等待佇列中轉移到同步佇列中,加入到對同步狀態的獲取中。 
int PROPAGATE = -3; // 表示下一次共享式同步狀態獲取將會無條件地被傳播下去。

在佇列中的管理,主要依靠這個int型的變數。
另外AQS中有兩個重要的成員變數:

private transient volatile Node head; 
private transient volatile Node tail;

由此可知,整個雙向連結串列是由頭尾節點來管理的。這樣更加方便執行緒的入隊和出隊操作。
那麼,節點如何進行入隊和出隊操作?實際上這對應著鎖的獲取和釋放兩個操作:獲取鎖失敗進行入隊操作,獲取 鎖成功進行出隊操作。

二、獨佔鎖ReentrantLock

現在我們來看一下ReentrantLock中的一些加鎖和解鎖操作的原始碼是怎樣的。

1.獨佔鎖的獲取

呼叫lock()方法是獲取獨佔鎖,獲取失敗就將當前執行緒加入同步佇列,成功 則執行緒執行。來看ReentrantLock原始碼:

public void lock() {
        sync.lock();
    }

ReentrantLock的lock呼叫了sync的lock,我們看看sync是怎麼來的:

//構造方法
public ReentrantLock() {
        sync = new NonfairSync();
    }
    
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

構造方法中由例項化了NonfairSync或FairSync的物件,它倆又繼承了靜態內部類Sync

static final class NonfairSync extends Sync 
static final class FairSync extends Sync

Sync中的lock是一個抽象方法:

abstract void lock();

所以在ReentrantLock中的lock看似是sync呼叫的lock,實則是由ReentrantLock的有參和無參構造方法決定的,sync子類所實現的lock方法(這裡以NonfairSync為例子)

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

lock方法使用CAS來嘗試將同步狀態改為1,如果成功則將同步狀態持有執行緒置為當前執行緒。否則將呼叫AQS提供的 acquire()方法

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

記住這個方法,下面將這個方法中的每個函式都解釋過去。

①.tryAcquire(arg)方法

再次嘗試獲取鎖,如果獲取成功,這個if條件直接跳出,執行緒獲取到鎖,如果失敗再執行後面的條件。看一下tryAcquire(arg)方法:

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

這是超類AQS中的,我們要看子類NonfairSync中所覆寫的方法:

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

呼叫了父類Sync中的nonfairTryAcquire(acquires)方法:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

通過原始碼可以發現這個方法,沒有排隊操作,直接判斷此時鎖的狀態是否可以獲取,或者此執行緒是否已經持有鎖(可重入)。如果都沒有,才返回false。

②addWaiter(Node.EXCLUSIVE)

如果tryAcquire返回false下一個呼叫的方法就是這個方法。先看傳入的引數:

static final Node EXCLUSIVE = null;

預設值是null。看一下這個方法的原始碼:

private Node addWaiter(Node mode) {
        // 將當前執行緒包裝稱為Node型別 
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 上面一句翻譯:嘗試enq的快速路徑;失敗時備份到完整enq 
        Node pred = tail;
         // 當前尾節點不為空 
        if (pred != null) {
            // 將當前執行緒以尾插的方式插入同步佇列中 
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 當前尾節點為空或CAS尾插失敗 
        enq(node);
        return node;
    }

分析上面的程式碼,首先清楚這個方法要幹嘛,剛才申請鎖失敗,所以這個方法要把當前執行緒放到同步佇列,等待獲取鎖。那麼剛進去就將當前執行緒封裝成一個節點,可以更好的管理。然後看看當前鎖佇列中的尾節點tail是否為null。
1.如果為空就直接呼叫enq方法。
2.如果不為空,就嘗試將當前執行緒的節點尾插到同步佇列中,如果插入成功說明排隊成功返回當前節點。否則插入失敗,呼叫enq方法
看一下enq方法的原始碼:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

我們已經知道此雙向連結串列是帶頭尾節點,而頭尾節點初始化的時機就是當尾節點tail為null的時候。所以在enq中進行死迴圈尾插,如果尾節點為null,就先用compareAndSetHead(new Node())方法初始化一個,當初始化成功,尾節點也被頭節點賦值,這時尾節點不為null,下一次迴圈就可以用CAS操作嘗試將本執行緒的節點尾插到同步佇列。因為是死迴圈,這個方法一定會等到尾插成功,才會返回尾節點。

③.acquireQueued(final Node node, int arg)

如果尾插成功,最終addWaiter(Node mode)返回當前插入執行緒的那個節點,接下來就呼叫acquireQueued,從名字就可以看出,這個方法是尾插隊成功的執行緒節點們排隊獲取鎖的過程,原始碼如下:

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);
                    // 釋放前驅節點 方便GC回收
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 獲取同步狀態失敗,執行緒進入等待狀態等待獲取獨佔鎖 
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

程式邏輯通過註釋已經標出,整體來看這是一個這又是一個自旋的過程(for ( ; ; )),程式碼首先獲取當前節點的先驅 節點,如果先驅節點是頭結點, 並且成功獲得同步狀態的時候(if (p == head && tryAcquire(arg))),當前節點所 指向的執行緒能夠獲取鎖。反之,獲取鎖失敗進入等待狀態。
那麼當獲取鎖失敗的時候會呼叫shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法,看看他們做了什 麼事情。shouldParkAfterFailedAcquire()方法原始碼為:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire()方法主要邏輯是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS將 節點狀態由INITIAL設定成SIGNAL,表示當前執行緒阻塞。當compareAndSetWaitStatus設定失敗則說明 shouldParkAfterFailedAcquire方法返回false,然後會在acquireQueued()方法中for (;;)死迴圈中會繼續重試,直至 compareAndSetWaitStatus設定節點狀態位為SIGNAL時shouldParkAfterFailedAcquire返回true時才會執行方法 parkAndCheckInterrupt()方法,該方法的原始碼為:

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

該方法的關鍵是會呼叫LookSupport.park()方法(關於LookSupport會在以後的文章進行討論),該方法是用來阻塞當 前執行緒的。因此到這裡就應該清楚了,acquireQueued()在自旋過程中主要完成了兩件事情:

  1. 如果當前節點的前驅節點是頭節點,並且能夠獲得同步狀態的話,當前執行緒能夠獲得鎖該方法執行結束退出;
  2. 獲取鎖失敗的話,先將節點狀態設定成SIGNAL,然後呼叫LookSupport.park方法使得當前執行緒阻塞。

小總結

至此獨佔鎖acquire方法的所有流程已經分析完畢,完成了對一個執行緒分配鎖和排隊的所有操作,總結圖如下:
在這裡插入圖片描述

2.獨佔鎖的釋放

獨佔鎖的釋放呼叫unlock方法,而該方法實際呼叫了AQS的release方法。下面來看這兩個方法的原始碼:

public void unlock() {
        sync.release(1);
    }
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返回true)則會執行if塊中的程式碼,tryRelease的原始碼還是看AQS子類Sync中的tryRelease

①.tryRelease方法

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

由原始碼可知,判斷當前執行緒是否是持有鎖的執行緒,如果不是就丟擲異常。如果是就判斷c==0,也就是說getState必須返回1。如果是,就將當前鎖的持有者設定為null,然後返回true,否則就改變setState狀態,然後返回false。
如果返回true說明有釋放鎖的條件,執行if語句中的程式碼塊。,當head指 向的頭結點不為null,並且該節點的狀態值不為0的話才會執行unparkSuccessor()方法。unparkSuccessor方法原始碼:

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            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);
    }

首先獲取頭節點的後繼節點,當後繼節點為null的時候會呼叫LookSupport.unpark()方法,該方 法會喚醒該節點的後繼節點所包裝的執行緒。因此,每一次鎖釋放後就會喚醒佇列中該節點的後繼節點所引用的線 程,從而進一步可以佐證獲得鎖的過程是一個FIFO(先進先出)的過程

三、獨佔鎖獲取和釋放的總結

  1. 執行緒獲取鎖失敗,執行緒被封裝成Node進行入隊操作,核心方法在於addWaiter()和enq(),同時enq()完成對同步隊 列的頭結點初始化工作以及CAS操作失敗的重試;
  2. 執行緒獲取鎖是一個自旋的過程,當且僅當 當前節點的前驅節點是頭結點並且成功獲得同步狀態時,節點出隊即 該節點引用的執行緒獲得鎖,否則,當不滿足條件時就會呼叫LookSupport.park()方法使得執行緒阻塞;
  3. 釋放鎖的時候會喚醒後繼節點;
  • 總體來說:在獲取同步狀態時,AQS維護一個同步佇列,獲取同步狀態失敗的執行緒會加入到佇列中進行自旋;移除 佇列(或停止自旋)的條件是前驅節點是頭結點並且成功獲得了同步狀態。在釋放同步狀態時,同步器會呼叫 unparkSuccessor()方法喚醒後繼節點。

四、Lock獨佔鎖的特性

還有中斷鎖lockInterruptibly()和超時等待鎖.tryLock(timeout,TimeUnit),這兩個方法也都是依靠AQS的原始碼實現的,如果學習了上面的原始碼,接下來這兩個原始碼就不難理解了,有興趣的可以自行檢視一下。

相關文章