ReentrantLock源碼分析與理解

在上面一篇分析ThreadExecutedPool的文章中我們看到線程池實現源碼中大量使用了ReentrantLock鎖,那麼ReentrantLock鎖的優勢是什麼?它又是怎麼實現的呢?
ReentrantLock又名可重入鎖,爲什麼稱之爲可重入鎖呢?簡單來說因爲它允許一個線程多次取獲得該鎖,不過多次獲取該鎖之後,也需要執行同樣次數的釋放鎖操作,否則該鎖將被當前線程一直持有,導致其它線程無法獲取。需要注意的是,釋放鎖的操作需要我們用代碼來控制,它並不會自動取釋放鎖。在ReentrantLock中實現了兩種鎖fairSync和NonfairSync,即公平鎖和非公平鎖,今天我們就來聊聊ReentrantLock中nonfairSync鎖的實現。
廢話不多說,下面開始分析代碼!

1、1 Lock()

首先看一下lock()方法,這個方法非常重要,它也是我們獲取鎖的入口:

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

ReentrantLock鎖的初始狀態爲0,compareAndSetState方法將嘗試獲取鎖並將當前鎖的狀態設置爲1。如果成功獲取了鎖會調用setExclusiveOwnerThread()方法設置當前線程擁有該鎖的獨佔訪問權。
如果調用compareAndSetState()獲取鎖失敗,則返回false並執行acquire(1)。

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

我們看到acquire(1)的代碼中有一條if語句,當tryAcquire(1)返回false以及acquireQueued(addWaiter(Node.EXCLUSIVE), arg)返回true時,纔會去執行selfInterrupt();方法。下面我們來看看tryAcquire(1)和acquireQueued(addWaiter(Node.EXCLUSIVE), arg)這兩個傢伙幹了什麼。

先看一下tryAcquire()方法。

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

tryAcquire方法會去調用nonfairTryAcquire()方法。

    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;
    }

1、首先調用getState()方法獲取當前鎖狀態,如果鎖狀態爲0。表示當前鎖沒有被其他線程佔用,這裏會再次嘗試去獲取鎖。如果成功的拿到了鎖,將設置鎖的擁有者爲當前線程,同時返回true。如果此時返回true的話,表示當前線程成功獲取到了鎖,lock()方法調用成功。
2、如果當前鎖狀態不爲0,判斷當前線程是否爲鎖的擁有者,如果是的話,嘗試將當前鎖的狀態值加acquires。如果當前neextc的值小於0,拋出異常。若不小於0,將當前鎖的值設置爲nextc。爲什麼說ReentrantLock爲可重入鎖,就體現在這裏了,如果當前線程爲鎖的擁有者,該線程再次調用lock方法時,當前鎖的狀態值會加1。當然我們釋放該鎖的時候,也要調用相應的unlock()方法,以使得鎖的state值爲0,可被其他線程請求。
3、如果當前鎖的值不爲0且擁有鎖的線程也不爲當前線程則返回false。也就是tryAcquire()再次獲取鎖並沒有成功。
值得注意的是,既然再第一次調用compareAndSetState()的時候,已經獲取失敗了爲什麼還要再調用tryAcquire()方法再獲取一次呢?我們可以理解爲這是一種保險機制,如果此時無法獲取鎖,我們將會將當前線程加入到阻塞隊列中掛起等待後面被喚醒重新爭奪鎖。

回顧一下上面的if判斷條件

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

如果tryAcquire()的返回值爲false,那麼接下來會執行acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法。這個方法看起來比較複雜,它在acquireQueued()方法又調用了addWaiter()方法,我們先來看看addWaiter()方法:

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

首先我們創建了一個包含了當前線程的Node節點,並將tail(tail節點即是尾節點)賦值給pred節點。如果我們第一次進來,那麼tail節點肯定爲空,將會去執行enq(node)方法。如果tail不爲空,那麼接下來的三句代碼幹了什麼呢?
先回憶一下,如果我們希望在一個雙向鏈表的尾部新增一個節點,應該如何操作,大致應該有如下三步:
- node.prev = pred; node節點的前驅指向尾節點
- pred.next = node; 將尾節點的後繼設置爲當前節點
- tail = node; 將node節點設置爲尾節點
我們再看一下詳細代碼:
1. node.prev=pred; 當前pred節點代表的是尾節點,也就是說設置node節點的前驅爲當前尾節點。
2. if (compareAndSetTail(pred, node)),我們看下compareAndSetTail(pred, node)方法。

    private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
    }
compareAndSetTail的原理其實就是CAS算法,將期望值和內存地址爲tailOffset上的值進行比較,如果兩者相同,則更新tailOffset上的值爲最新值update。其實也就是如果tailOffset上的值和pred(老的尾節點)的值相同,則將尾節點更新爲新的node節點。

3. 將原尾節點的後繼設置爲當前節點。
其實上面三步實現的功能和在雙向鏈表尾部新增一個節點的功能大致相同,只是順序略有調整。

接下來看一下enq(node)方法

    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;
                }
            }
        }
    }

enq代碼中有一個死循環for(;;),在這個循環中會執行以下操作:
1. 將tail節點賦給t節點,t節點當前即爲尾節點。
2. 如果爲t節點爲空,將當前節點設置爲頭節點,並將頭節點賦值給尾節點。相當於頭尾節點都指向了新建節點。
3. 如果尾節點不爲空,將當前節點node的前驅指向尾節點,將node節點設置爲新的tail節點。同時將原尾節點的後繼設置爲當前節點,相當於將當前node節點鏈接到原尾節點之後,插入到鏈表中。

看到現在,我們大致明白了addWaiter()方法其實就是將當前節點添加到鏈表尾部的一個方法。

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

再跳回到這句代碼,現在已經將線程添加到隊列中了。那麼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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在acquireQueued方法中有一個無限循環,這個循環幹了什麼,它的用處是什麼呢?我們接着看

final Node p = node.predecessor();

獲取當前node節點的前驅節點,並賦值給p

    if (p == head && tryAcquire(arg)) {
        setHead(node);
        p.next = null; // help GC
        failed = false;
        return interrupted;
    }
  1. 如果p節點爲頭結點,同時當前線程可以獲取到鎖,則調用setHead(node)方法。setHead方法其實即是將當前獲取到了鎖的節點設置爲頭結點。如果當前節點已經獲取到了鎖,那麼該節點也無需再保存當前線程了。此時當前線程已經獲取到了鎖,將p節點的後繼節點設置爲null,以方便jvm自動回收。最後跳出當前循環。
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
  1. 如果當前節點的前驅不爲頭節點,或當前節點無法獲取到鎖。執行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;
    }

上面的代碼可以總結爲一下三句話:
- 如果前驅節點的waitStatus等於Node.SIGNAL,表示前驅節點已經準備好去喚醒後續節點。因此我們可以安全的將當前線程掛起以待被喚醒獲得鎖。返回true,當前線程在後續代碼中被掛機。
- 如果前驅節點的waiteStatus狀態大於0(只有CANCEL狀態值纔會大於0),從當前節點一直往前找,直到找到一個waitStatus狀態小於0的節點。將找到節點的後繼設置爲當前節點。
- 如果前驅節點的狀態即不爲SIGNAL也大於0,將前驅節點的狀態設置爲SIGNAL。

到此爲止,我們已經知道了嘗試去獲取鎖的線程是如何被放入到阻塞隊列中並掛起的。接下來我們來看看獲取到鎖的線程是如何釋放鎖的。

1、2 unlock()

上面我們已經較爲清晰的理了一遍ReentrantLock獲取鎖的思路,下面我們開始分析一下如何釋放獲取到的鎖。
在ReentrantLock裏面有如下代碼,我們跟蹤一下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;
    }

在release()方法中,首先嚐試去執行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;
    }
  1. 獲取當前鎖的狀態值並減去傳遞過來的releases變量,不難發現。如果我們在獲取鎖的過程中會將當前鎖的state值加1,因此我們在釋放的時候也需要相對應的將state的值減去1。
  2. 如果當前線程不爲鎖的擁有者,拋出異常。我們的目的是釋放當前線程擁有的鎖,這句代碼也很好理解。
  3. 創建一個初始值爲false的變量free用來標識當前鎖的操作是否被釋放。
  4. c的值等於當前鎖的狀態值減去releasesd。由於ReentrantLock鎖是可重入鎖,因此鎖的state值有可能是大於1的值。然而當state的值不爲0的時候,我們可以任務當前線程仍然持有該鎖,其它線程依然不能夠去調用lock()方法去獲取該鎖。如果c的值等於0,我們任務當前線程已經釋放了該鎖,其它線程可以開始爭奪它了。因爲我們將free的值設置爲true,同時將當前鎖的擁有者設置爲null。
  5. setState(c);吳磊當前鎖的state值是否爲0,我們都需要去更新state值。
  6. 返回free值。如果free的值爲true,表示當前線程已不再擁有該鎖,我們可以去喚醒後繼線程來爭奪該鎖了。如果free的值爲false,表示該鎖仍然被當前線程所持有。

繼續看上面的代碼

    Node h = head;
    if (h != null && h.waitStatus != 0)
        unparkSuccessor(h);
    return true;

如果當前線程所持有的鎖的state值爲0,那麼此時需要喚醒當前線程的後繼節點去爭奪該鎖。我們看一下這段代碼幹了什麼:
1. 新建一個h節點,並將其賦值爲head節點。
2. 如果h節點爲空,意味着當前頭節點爲空,一般情況頭結點我們可以理解爲即是當前擁有鎖的線程,既然當前節點爲空,那麼也就沒有辦法釋放鎖了。同時如果頭節點的waitStatus爲0,正常擁有鎖或者已經釋放鎖打算去喚醒其它線程的節點,不會爲0狀態。如果頭節點不爲空,且waitStatus也不爲0,調用unparkSuccessor(h);方法去喚醒後繼節點。
3. 返回true

那麼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);
    }
  1. 定義一個ws變量用於當前節點的waits狀態。
  2. 如果waitStatus值小於0,將當前node節點的值設置爲0;當然有人會問如果waitStatus的值大於0怎麼辦呢?如果waitSatus的值大於0,那麼它只可能爲CANCEL狀態,也就是說當前節點線程已經被取消,自然也不用去喚醒別人了。
  3. 定義一個s節點擁有保存頭節點的後繼。
  4. 如果s==null,表示後繼不存在,那麼我們就要去嘗試找找有沒有其它線程等待被喚醒了;如果s.waitStatus小於0,小於0也就表示當前節點不爲CANCLE狀態,可以被喚醒。那麼如果當前節點爲空或者waitStatus值大於0時,如何去獲取後續節點呢?下面的一大串代碼其就是從當前隊列的尾節點開始往前找,找到一個離當前節點最近的且waitStatus值小於0的節點等待喚醒的過程;
  5. 如果s節點不爲null,調用LockSupport.unpark(s.thread);去喚醒該節點!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章