ReentrantLock介紹及源碼解析

ReentrantLock介紹及源碼解析

一、ReentrantLock介紹

  • ReentrantLock是JUC包下的一個併發工具類,可以通過他顯示的加鎖(lock)和釋放鎖(unlock)來實現線程的安全訪問,ReentrantLock還可以實現公平鎖和非公平鎖,並且其與synchronized的作用是一致的,區別在於加鎖的底層實現不一樣,寫法上也不一樣,具體異同可以參見下圖:

二、ReentrantLock的源碼簡析

1、源碼分析

  • ReentrantLock(下面簡稱RL)就是AQS獨佔鎖的一個典型實現,其通過維護state變量的值來判斷當前線程是否能夠擁有鎖,如果通過cas將state成功從0變成1表示爭用資源成功,否則表示爭用失敗,進入CLH隊列,通過CLH隊列來維護那些暫時沒搶佔到鎖資源的線程;其內部維護了一個名爲Sync的內部類來繼承AQS,又因爲RL既可以支持公平鎖也可以支持非公平鎖,所以其內部還維護了兩個內部類FairSync和NonfairSync來繼承Sync,通過他們來實現AQS的模板方法從而實現加鎖的過程;類的關係圖如下:

  • 公平鎖和非公平鎖在源碼層的兩點區別:

    1、非公平上來直接搶鎖

    2、當state=0時,非公平直接搶,公平鎖還會判斷隊列還有沒有前置節點

2、lock方法的源碼跟蹤

下面就讓我們跟蹤RL的lock()和unLock()源碼來看看代碼級別是怎麼實現的吧!

需要注意的是,本文跟蹤的是非公平鎖的加解鎖過程,公平鎖的實現大體一致,當源碼中有與公平鎖的顯著差別時我會通過註釋給出解釋

  • 試用例如下
public static void main(String[] args) throws InterruptedException {
    long start = System.currentTimeMillis();
    List<Thread> list = new ArrayList<>();
    ReentrantLock lock = new ReentrantLock();
    for (int i = 0; i < 1000; i++) {
        Thread thread = new Thread(()-> {
            for (int j = 0; j < 1000; j++) {
                // 解鎖
                lock.lock();
                count++;
                // 釋放鎖
                lock.unlock();
            }
        });
        list.add(thread);
    }
    for (Thread thread : list) {
        thread.start();
    }
    for (Thread thread : list) {
        thread.join();
    }
    System.out.println("auto.count = " + count + "耗時:" + (System.currentTimeMillis() -start));
}

(1)、lock()的源碼跟蹤與解析

跟蹤lock.lock()發現其調用的是內部類Sync的lock()方法,該方法是一個抽象方法,具體實現由FairSync和NonfairSync實現,由於我們構造RL時調用的是無參構造函數,所以這裏會直接進入NonfairSync的lock()方法;具體實現代碼和註釋如下:

/**
 * java.util.concurrent.locks.ReentrantLock.NonfairSync#lock()
 */
final void lock() {
    // 由於是非公平鎖所以這裏上來直接爭搶資源,嘗試通過CAS操作將state的值由0變成1
    if (compareAndSetState(0, 1)) 
        // 如果成功將state值變成1表示爭搶鎖成功,設置當前擁有獨佔訪問權的線程。
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 爭搶失敗再進入與公平鎖一樣的排隊邏輯
        acquire(1);
}

tips:

1、上面的compareAndSetState方法也是由AQS提供的,裏面藉助Unsafe實現了對state的cas操作更新

2、setExclusiveOwnerThread也可以理解成由AQS提供(其實是AQS的父類,不過不影響理解),給exclusiveOwnerThread變量賦值,exclusiveOwnerThread表示當前正在擁有鎖的線程

3、acquire方法同樣由AQS提供,其內部實現也是lock環節比較關鍵的代碼,下面我會詳細解釋

(2)、acquire()的源碼跟蹤與解析

acquire方法的源碼如下:

/**
 *  java.util.concurrent.locks.AbstractQueuedSynchronizer#acquire(int)
 */
public final void acquire(int arg) {
    /**
     * 1、嘗試獲取鎖;如果成功此方法結束,當前線程執行同步代碼塊
     * 2、如果獲取失敗,則構造Node節點並加入CLH隊列
     * 3、然後繼續等待鎖
     */
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 如果獲取鎖失敗,添加CLH隊列也失敗,那麼直接中斷當前線程
        selfInterrupt();
}

tips:

1、tryAcquire方法是AQS的一個模板方法,RL下的公平和非公平鎖都有不同的實現,下面會詳解

2、addWaiter方法是AQS的一個默認實現方法,負責構造當前線程所在的Node,並將其設置到隊列的尾巴上

3、acquireQueued方法也是AQS的默認實現,旨在設置CLH隊列的head和阻塞當前線程

上面的三個方法下面也會一一介紹

(3)、tryAcquire()的源碼跟蹤與解析

  • tryAcquire()方法可以理解成嘗試獲取鎖,如果獲取成功即表示當前線程擁有了鎖;跟蹤源碼需要注意的一點是:該方法在非公平鎖(NonFairSync)下的實現最終調用的是Sync裏的nonfairTryAcquire方法,所以我們直接觀察該方法是如何實現的即可
/**
 * java.util.concurrent.locks.ReentrantLock.Sync#nonfairTryAcquire(int)
 */
final boolean nonfairTryAcquire(int acquires) {
    // 當前線程
    final Thread current = Thread.currentThread();
    // 獲取當前state的值
    int c = getState();
    if (c == 0) {
        /**
         * 非公平鎖發現資源未被佔用時直接CAS嘗試搶佔資源;而公平鎖發現資源未被佔用時
         * 先判斷隊列裏是否還有前置節點再等待,沒有才會去搶佔資源
         */
        if (compareAndSetState(0, acquires)) {
            // 如果成功將state值變成1表示爭搶鎖成功,設置當前擁有獨佔訪問權的線程。
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    /** 
     * 如果state!=0表示有爭用,再判斷當前系統擁有獨佔權限的線程是不是當前線程,
     * 如果是,則需要支持線程重入,將state的值加1
     */
    else if (current == getExclusiveOwnerThread()) {// 處理可重入的邏輯
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // state既不等於0也不需要重入則返回false;表示獲取鎖失敗,代碼返回後繼續執行acquireQueued方法
    return false;
}

(4)addWaiter()的源碼跟蹤與解析

  • 執行到addWaiter方法表示前面的tryAcquire嘗試獲取鎖失敗了,需要由此方法構建Node節點並加入到CLH隊列的末尾;此方法返回的Node即爲當前CLH隊列的tail節點
/**
 * java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter(java.util.concurrent.locks.AbstractQueuedSynchronizer.Node)
 */
private Node addWaiter(Node mode) {
    // 構建Node對象
    Node node = new Node(Thread.currentThread(), mode);
    /**
     * 將當前隊列的尾節點賦值給pred,通過命名和下面的代碼其實可以發現就是想讓tail作爲當前節點的前置節點;
     * 但是爲什麼不直接用tail而將其賦值給pred再用呢?我想應該是考慮併發環境下tail的引用有可能會被其他線程改變
     */
    Node pred = tail;
    if (pred != null) {
        // 如果當前隊列的尾結點(tail)不爲空,就將其作爲當前Node節點的前置節點
        node.prev = pred;
        // 然後通過AQS自帶的cas方法將當前構建的Node節點插入到隊列的尾巴上
        if (compareAndSetTail(pred, node)) {
            // 如果成功了,前置節點也就是之前的tail節點的後繼節點就是當前節點,賦值
            pred.next = node;
            // 返回構建的Node節點,即當前隊列的tail節點
            return node;
        }
    }
    // 如果隊列的tail節點爲空,或者cas設置tail節點失敗的話調用此方法;旨在重新設置隊列的tail節點
    enq(node);
    return node;
}

(5)、acquireQueued()的源碼跟蹤與解析

  • 當線程通過tryAcquire上鎖失敗,然後通過addWaiter將當前線程添加到隊列末尾後,通過此方法再次判斷是否輪到當前節點,並再次嘗試獲取鎖,獲取不到的話進行阻塞操作,源碼與註釋如下:
/**
 * java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued(java.util.concurrent.locks.AbstractQueuedSynchronizer.Node, int)
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 獲取tail節點的前置節點
            final Node p = node.predecessor();
            /**
             * 如果前置節點就是頭節點表示當前tail節點就是第二個節點,就可以嘗試着去獲取鎖,
             * 然後將tail節點設置成頭節點,返回線程中斷狀態爲false;表示當前線程獲取到鎖
             */
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                /**
                 * 既然tail已經獲取到鎖了,那麼前置節點就沒用了,這裏將前置節點的next設置爲空,
                 * 是爲了方便垃圾回收,因爲如果不指定爲空,前置節點的next就是當前的tail節點,
                 * 不會被回收
                 */
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /**
             * 如果前置節點不爲head,或者雖然前置節點是head但是獲取鎖失敗,那麼就
             * 需要在這裏將線程阻塞,阻塞利用的是LockSupport.park(thread)來實現的
             */
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 退出獲取鎖
            cancelAcquire(node);
    }
}

至此,RL非公平鎖加鎖的過程的源碼跟蹤完畢,流程也不算複雜,下面簡單梳理一遍:

1、上來直接嘗試獲取鎖(修改state值),成功表示獲取成功

2、否則執行tryAcquire方法嘗試通過cas的方式獲取鎖,並處理可能存在的重入操作

3、獲取失敗則通過addWriter方法構建Node節點並加入CLH隊列的末尾

4、然後在acquireQueued裏再次獲取鎖,獲取失敗則阻塞當前線程;

下面簡單畫了一下lock()方法的調用泳道圖

1、調用父類AQS的compareAndSetState通過cas的模式嘗試將state狀態改爲1,修改成功則持有鎖,將當前線程設爲ExclusiveOwnerThread

3、unLock方法的源碼跟蹤

  • 釋放鎖其實就是將state狀態減1,然後處理可重入邏輯,如果沒有重入的話直接喚醒當前隊列的head節點,把當前線程所在的Node節點從隊列中剔除
  • unLock方法對應AQS的tryRelease模板方法的實現,其沒有lock那麼複雜,因爲不用支持公平和非公平鎖,所以其可以直接在sync中調用AQS提供的release方法,然後觸發tryRelease,調用sync裏的tryRelease實現從而實現解鎖

AQS的release源碼

/**
 * java.util.concurrent.locks.AbstractQueuedSynchronizer#release(int)
 */
public final boolean release(int arg) {
    // 嘗試釋放鎖
    if (tryRelease(arg)) {
        // 釋放成功,判斷當前隊列頭節點是否爲空,不爲空並且等待狀態不等於0則喚醒當前隊列的頭節點
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

RL的tryRelease實現

/**
 * java.util.concurrent.locks.ReentrantLock.Sync#tryRelease(int)
 * @param releases
 * @return
 */
protected final boolean tryRelease(int releases) {
    // state減1
    int c = getState() - releases;
    // 如果當前線程不是正在獲取到鎖的線程直接拋異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果state減1後等於0表示沒有重入,表示釋放鎖成功,將當前獲取鎖的線程置空
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 將最新的state狀態更新到AQS中
    setState(c);
    return free;
}

unlock()總結:

1、調用父類AQS的release方法實際調用的是tryRelease這個模板方法由ReentrantLock本身實現

2、tryRelease方法嘗試將state減1,如果減完等於0表示解鎖成功,將ExclusiveOwner線程設爲空;並且喚醒隊列的頭節點(unparkSuccessor)。

3、如果不等於0表示解鎖失敗,將state設爲減1過後的值;也是爲了可重入

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章