Java鎖之ReentrantLock(二)

木木匠發表於2018-08-17

一、前言

上一篇《Java鎖之ReentrantLock(一)》已經介紹了ReentrantLock的基本原始碼,分析了ReentrantLock的公平鎖和非公平鎖機制,最終分析ReentrantLock還是依託於AbstractQueuedSynchronizer同步佇列器(以下簡稱同步器)實現,所以本篇開始分析同步器內部的程式碼實現,考慮到程式碼結構比較長,所以分析原始碼時會精簡部分不重要的程式碼,但是最終還是會以不影響程式碼邏輯的情況下進行精簡。

二、同步器分析

  • 同步器主要屬性

Java鎖之ReentrantLock(二)

根據上圖原始碼我們可以知道,AbstractQueuedSynchronizer內部構建了一個Node節點物件,同時構造了一個具有volatile屬性頭節點與尾部節點,保證了多執行緒之間的可見性,同時最重要的是定義了一個int型別變數state,通過上一篇文章分析,我們知道了ReenTrantLock是否獲取到鎖的判斷就是state是否大於0,等於0表示鎖空閒,大於0,表示鎖已經被獲取。接下來我們重點分析下Node節點內部構造以及同步器的實現原理,Node原始碼如下:



        static final class Node {
        //共享模式
        static final Node SHARED = new Node();
        //獨佔模式
        static final Node EXCLUSIVE = null;
        //取消狀態
        static final int CANCELLED =  1;
        //喚醒狀態
        static final int SIGNAL    = -1;
        //等待條件狀態
        static final int CONDITION = -2;
       //傳播狀態
        static final int PROPAGATE = -3;

       //等待狀態
        volatile int waitStatus;

       //上一個節點
        volatile Node prev;

      //下一個節點
        volatile Node next;

      //獲取同步狀態的執行緒
        volatile Thread thread;

        //等待佇列中的後繼節點,如果當前節點是共享的,那麼這個nextWaiter=SHARED
        Node nextWaiter;

        //判斷當前後繼節點是否是共享的
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回當前節點的前一個節點
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }
複製程式碼

這裡需要重點說明的屬性是waitStatus,該狀態就是包括節點內部宣告的幾個常量,如下:

常量名 功能
CANCELLED 值為1,當前節點進入取消狀態,原因是由於被中斷或者是等待超時而進入取消狀態,需要說明的是,節點執行緒進入取消狀態後,狀態不會再改變,也就不會再阻塞獲取鎖
SIGNAL 值為-1,後繼節點的執行緒處於等待狀態,而當前節點的執行緒如果釋放了同步狀態或者被取消,將會通知後繼節點,使得後繼節點的執行緒得以執行
CONDITION 值為-2,節點在等待佇列中,節點執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal()方法後,該節點將會從等待佇列中轉移到同步佇列中,加入到同步狀態的競爭中
PROPAGATE 值為-3,表示下一次共享式同步狀態獲取會無條件的傳播下去,比如如果頭節點獲取到共享式同步狀態,判斷狀態是PROPAGATE,會繼續呼叫doReleaseShared,使得後繼節點繼續獲取鎖
INITIAL 值為 0,表示初始狀態(這個應該是老版本中的程式碼中存在,目前檢視jdk1.8已經沒有顯示宣告INITIAL狀態,因為初始化時候,int變數預設就是0)

分析同步器的屬性,我們可以大概畫出構造器的佇列示意圖,如下:

Java鎖之ReentrantLock(二)

首先同步器宣告瞭頭節點和尾部節點,head節點指向一個node節點表示該節點是佇列的頭部節點,tail節點指向一個node節點表示該節點是尾部節點,同時,每個節點都有pre和next屬性,指向node節點,然後如圖所示構建成一個FIFO雙向連結串列式佇列。下面我們檢視下同步器常用主要方法

  • 同步器主要方法列表

方法名稱 功能
compareAndSetState(int expect, int update) CAS進行設定同步狀態
enq(final Node node) 迴圈入等待佇列,直到入隊成功為止
addWaiter(Node mode) 以當前執行緒建立一個尾部節點,並加入到尾部
unparkSuccessor(Node node) 喚醒節點的後繼節點
doReleaseShared() 釋放共享模式下的同步狀態
setHeadAndPropagate(Node node, int propagate) 設定頭節點,並繼續傳播同步許可
release(int arg) 獨佔式釋放同步狀態
acquireShared(int arg) 共享式釋放同步狀態
hasQueuedPredecessors() 判斷是否有比當前執行緒等待更久的執行緒(用於公平鎖)
  • 同步器加鎖

以上是同步器主要的方法,我們接下來會對上述部分方法進行重點分析,要了解同步器如何完成加鎖,等待獲取鎖,釋放鎖的功能,我們先回顧上一篇文章分析ReentranLock的Lock()方法,實現原始碼如下:

  /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
複製程式碼

我們發現重點是acquire(1)方法,該方法是父類也就是同步器提供的,原始碼如下:

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

原始碼其實可以拆分為三部分:

  • tryAcquire(arg) 嘗試獲取鎖
  • addWaiter(Node.EXCLUSIVE), arg) 以當前執行緒構建成節點新增到佇列尾部
  • acquireQueued(final Node node, int arg)讓節點以死迴圈去獲取同步狀態,獲取成功就退出迴圈

其實解析為三部分就很清楚這個方法的作用了,首先嚐試獲取鎖,獲取不到就把自己新增到尾部,然後在佇列中死迴圈去獲取鎖,最重要的部分就是acquireQueued(final Node node, int arg),原始碼如下:

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);
        }
    }
複製程式碼
//該方法主要靠前驅節點判斷當前執行緒是否應該被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 如果前任節點的狀態等於SIGNAL,
             * 說明前任節點獲取到了同步狀態,當前節點應該被阻塞,返回true
             */
            return true;
        if (ws > 0) {
            /*
             * 前任節點被取消
             */
            do {//迴圈查詢取消節點的前任節點,
            //直到找到不是取消狀態的節點,然後剔除是取消狀態的節點,
            //關聯前任節點的下一個節點為當前節點
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * CAS設定前任節點等待狀態為SIGNAL,
             * 設定成功表示當前節點應該被阻塞,下一次迴圈呼叫就會
            *  return  true
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
複製程式碼
//把當前執行緒掛起,從而阻塞住執行緒的呼叫棧,
//同時返回當前執行緒的中斷狀態。
//其內部則是呼叫LockSupport工具類的park()方法來阻塞該方法。
   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//阻塞執行緒
        return Thread.interrupted();
    }
複製程式碼

以上就是lock.lock()的加鎖過程,我們總結分析下:

  • 首先,直接嘗試獲取鎖,獲取成功直接結束。
  • 如果獲取鎖失敗,就把當前執行緒構造一個尾部節點,CAS方式加入到佇列的尾部
  • 在佇列中,死迴圈式的判斷前任節點是否是頭節點,如果是頭節點就嘗試獲取鎖,如果不是就把自己掛起,等待前任節點喚醒自己,這樣可以避免多個執行緒死迴圈帶來的效能消耗。
  • 同步器解鎖

    lock.unlock()釋放鎖的過程,分析原始碼,老規矩,上原始碼:
    //釋放鎖
   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;
    }
    //該方法和之前分析的程式碼類似,主要是設定Sate狀態
     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);//設定同步狀態佔有執行緒為null
            }
            setState(c);
            return free;
        }
        
    private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 取到當前節點的下一個節點,如果節點不為空就喚醒阻塞的執行緒
        //如果節點為空,或者節點是取消狀態,那麼就迴圈從尾部節點找到
        //當前節點的下一個節點喚醒
        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);//喚醒阻塞的執行緒
    }    
複製程式碼

以上就是對lock.unlock()分析,同樣我們總結分析下

  • 首先既然加鎖成功與否判斷是根據State不為0來判斷,所以,釋放鎖就會把State設定為0,同時設定鎖的所有者執行緒為null
  • 鎖釋放成功了,接著就會喚醒在佇列的後繼節點,通過呼叫LockSupport.unpark(s.thread)來喚醒執行緒的,LockSupport主要依託於sun.misc.Unsafe類來實現的,該類提供了作業系統硬體級別的方法,不在本文討論中。

三、尾言

  • 本次主要分析了AQS同步器的加鎖和解鎖的實現,其實jdk很多同步工具類都是依賴於AQS同步器實現的,瞭解了AQS同步器的原理後,對理解其他併發工具的原理也很有幫助,比如CountDownLatchSemaphore,CyclicBarrier(依賴ReentrantLock)等。下一篇《Java鎖之ReentrantReadWriteLock》繼續細化分析,分析讀鎖和寫鎖,分析ReentrantLock是如何實現讀鎖的重複獲取,鎖降級等功能的。

相關文章