ReentrantLock源碼解析——雖衆但寫

在看這篇文章時,筆者默認你已經看過AQS或者已經初步的瞭解AQS的內部過程。html

  先簡單介紹一下ReentantLock,跟synchronized相同,是可重入的重量級鎖。可是其用法則至關不一樣,首先ReentrantLock顯式的調用lock方法表示接下來的這段代碼已經被當前線程鎖住,其餘線程須要執行時須要拿到這個鎖才能執行,而當前線程在執行完以後要顯式的釋放鎖,固定格式java

lock.lock();
try {
    doSomething();
} finally {
    lock.unlock();
}

1.ReentrantLock的demo程序

來經過下面這段代碼簡單的瞭解ReentrantLock是如何使用的框架

// 定義一個鎖
	private static Lock lock = new ReentrantLock();

    /**
     * ReentrantLock的使用例子,而且驗證其一些特性
     * @param args 入參
     * @throws Exception 錯誤
     */
    public static void main(String[] args) throws Exception {
        // 線程池
        ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();

        executor.execute(() -> {
            System.err.println("線程1嘗試獲取lock鎖...");
            lock.lock();
            try {
                System.err.println("線程1拿到鎖並進入try,準備執行testForLock方法");
                // 調用下方的方法,驗證lock的可重入性
                testForLock();
                TimeUnit.MILLISECONDS.sleep(500);
                System.err.println("線程1try模塊所有執行完畢,準備釋放lock鎖");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.err.println("線程1釋放lock鎖,線程1釋放鎖2次,此時纔算真正釋放,驗證了ReentrantLock加鎖多少次就要釋放多少次鎖");
            }
        });

        // 先睡他100ms,保證線程1先拿到鎖
        TimeUnit.MILLISECONDS.sleep(100);
        
        executor.execute(() -> {
            System.err.println("線程2嘗試獲取lock鎖...");
            lock.lock();
            try {
                System.err.println("線程2拿到鎖並進入try");
            } finally {
                lock.unlock();
                System.err.println("線程2執行完畢,釋放lock鎖");
            }
        });

    }

    /**
     * 驗證ReentrantLock具備可重入
     */
    public static void testForLock() throws InterruptedException {
        System.err.println("線程1開始執行testForLock方法,正準備獲取lock鎖...");
        lock.lock();
        try {
            System.err.println("testForLock成功獲取lock鎖,證實了ReentrantLock具備可重入性");
            TimeUnit.MILLISECONDS.sleep(200);
        } finally {
            lock.unlock();
            System.err.println("testForLock釋放lock鎖,線程1釋放鎖一次");
        }
    }

結果圖:1585664568146ui

  從結果圖中,咱們獲得了不少信息,好比ReentrantLock具有可重入性(testForLock方法得出),而且其釋放鎖的次數必須跟加鎖的次數保持一致(這樣才能保證正確性);此外ReentrantLock悲觀鎖,在某個線程獲取到鎖以後其餘線程在其徹底釋放以前不得獲取(線程2充分證實了這一點,其開始獲取鎖的時間要比線程1的執行時間快許多,但仍是被阻塞住了)。線程

2.獲取鎖的方法——lock()

  okay,那來看下其內部是如何實現的,直接點擊lock()方法code

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

看到其直接調用了synclock()方法,再點擊進入htm

abstract static class Sync extends AbstractQueuedSynchronizer {
    // ...
    
    abstract void lock();
    
    // ...
}

  能夠看到Sync類是ReentrantLock的一個內部類,繼承了AQS框架,也就是說ReentrantLock就是AQS框架下的一個產物,那麼問題就變得簡單起來了。若是還沒了解過AQS的能夠看下我另外一篇文章——AQS框架詳解,看過以後再回頭看ReentrantLock,你會發現,就這?blog

  扯回來ReentrantLock,這邊能夠看到內部類Sync是一個抽象類,lock()方法也是一個抽象方法,也就意味着這個lock會根據子類的不一樣實現執行不一樣操做,點開子類發現有兩個——公平鎖和非公平鎖繼承

1585667693048

裏邊的具體實現先放一放,回到ReentrantLocklock方法隊列

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

  直接調用說明sync已經被初始化過,那麼在哪裏進行初始化的呢?仔細翻一翻能夠從ReentrantLock兩個構造方法中發現貓膩

/**
 * 構造方法1
 * 無參構造方法,直接將sync初始化爲非公平鎖
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * 構造方法2
 * 帶參構造方法,根據傳進來的布爾值決定將sync初始化爲公平仍是非公平鎖
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

  這裏順帶說一下,在AQS有一個同步隊列(CLH),是一種先進先出隊列。公平鎖的意思就是嚴格按照這個隊列的順序來獲取鎖,非公平鎖的意思就是不必定按照這個隊列的順序來。

  那如今知道sync是在建立ReentrantLock的時候就進行了初始化,咱們就來看下公平和非公平鎖各自作了什麼吧。

2.1 非公平鎖

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    final void lock() {
        // 使用CAS嘗試將state改成1,若是成功了,則表示獲取鎖成功,設置當前線程爲持有線程便可
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 不然的話調用AQS的acquire方法乖乖入同步隊列等待去吧
            acquire(1);
    }

    // AQS暴露出來須要子類重寫的方法
    protected final boolean tryAcquire(int acquires) {
        // 方法解釋在下方
        return nonfairTryAcquire(acquires);
    }
}

// 非公平鎖的tryAcquire方法,該方法是放在Sync抽象類中的,爲了tryLock的時候使用
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    // 當前鎖的狀態
    int c = getState();
    // 若是是0則表示鎖是開放狀態,能夠爭奪
    if (c == 0) {
        // 使用CAS設置爲對應的值,在ReentrantLock中acquires的值一直是1
        if (compareAndSetState(0, acquires)) {
            // 成功了設置持有線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    /* 
     * 若是當前線程是持有線程,那麼state的值+1
     * 這裏也是ReentrantLock可重入的原理
     */
    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;
}

  非公平鎖基本的流程解釋在上方的代碼中已經在註釋寫出,相信不難看懂。不過有個須要注意的點要說一下,首先要看清楚非公平鎖的定義,它是不必定按照隊列順序來獲取,不是不按照隊列順序獲取。

  從上面的代碼咱們也能夠看出來,非公平鎖調用lock()方法的時候會先調用一次CAS來獲取鎖,成功了直接返回,這第一次操做沒有按照隊列的順序來,但也只有這一次。若是失敗了,入隊以後仍是乖乖的得按照CLH同步隊列的順序來拿鎖,這一點要搞清楚。

2.3 公平鎖

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    // lock方法直接調用AQS的acquire方法,連一點爭取的慾望都沒有
    final void lock() {
        acquire(1);
    }

    // 公平鎖的獲取資源方法,該方法是在acquire方法類調用的
    protected final boolean tryAcquire(int acquires) {
        
        // 總體邏輯仍是挺簡單的,跟非公平有些相似
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            /*
             * c==0表示當前鎖沒有被獲取
             * 若是沒有前驅節點或者前驅節點是頭結點,
             * 那麼使用CAS嘗試獲取資源
             * 成功了設置持有線程並返回true,失敗了直接返回
             */
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 若是當前線程持有鎖,跟非公平鎖一致,可重入
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

  公平鎖的邏輯相對來講十分簡單,lock方法老老實實的去排隊獲取鎖,而獲取資源方法的邏輯也在代碼註釋寫得很清楚了,沒有什麼須要多講的。

3.鎖釋放

上面的理解以後釋放鎖的邏輯就簡單的多了,直接放代碼吧:

/*
 * 解鎖方法直接調用AQS的release方法
 * 而release方法的去向又是跟tryRelease的返回值直接相關
 * tryRelease方法的實如今內部類Sync中,具體在下方
 */
public void unlock() {
    sync.release(1);
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    // ...

    // 釋放資源的方法
    protected final boolean tryRelease(int releases) {
        // 拿到當前鎖的加鎖次數
        int c = getState() - releases;
        // 當前線程必須是鎖持有線程才能操做
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 若是次數爲0,表示徹底釋放,清空持有線程
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

 	// ...
}

  釋放鎖的邏輯在註釋中解釋得很清楚了,看完也知道因爲ReentrantLock是可重入的,因此鎖的數值會逐漸增長,那麼在釋放的時候也要一個一個逐一釋放

主要的邏輯仍是AQSrelease方法中,這裏詳講的話篇幅太多,有興趣的話能夠單獨看下AQS的文章,傳送門:AQS

4.ReentrantLock的可選擇性

  來說下ReentrantLockSynchonized的一大不一樣點之一——Condition。那麼condition是什麼呢,簡單來講就是將等待獲取資源的線程獨立出來分隊,什麼意思呢?舉個例子,如今有8個線程同時爭取一個鎖,我以爲太多了,就把這個8個線程平均分紅4隊,等我以爲哪隊OK就將那一隊的線程叫出來爭取這個鎖。在這裏的condition就是隊伍,4隊就是4個condition

  另外說一句,condition(隊伍)中的線程是不參與鎖的競爭的,若是上方的8個線程我只將2個線程放入一個隊,其餘線程不創建隊伍,那麼其餘線程會參與鎖的競爭,而獨立到隊伍中的2個線程則不會,由於其被放在AQS等待隊列中,等待隊列是不參與資源的競爭的,我在另外一篇文章——AQS框架詳解寫得很清楚了。仍是那句話,AQS懂了再看ReentrantLock,理解難度就會低得多得多得多得多....

okay,那來簡單看下Condition如何使用

// 線程池
ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();
// 這裏只建了一個condition起理解做用,本身有興趣的話能夠多建幾個模擬多點場景
Condition condition = lock.newCondition();

executor.execute(() -> {
    System.err.println("線程1嘗試獲取lock鎖...");
    lock.lock();
    try {
        System.err.println("線程1拿到鎖並進入try");
        System.err.println("線程1準備進行condition操做");
        /*
         * 將當前線程即線程1放入指定的這個condition中,
         * 若是是其餘condition則調用其餘condition的await()方法
         */
        condition.await();
        System.err.println("線程1結束condition操做");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        lock.unlock();
        System.err.println("線程1執行完畢,釋放lock鎖");
    }
});
// 保證線程1獲取鎖而且執行完畢
TimeUnit.MILLISECONDS.sleep(200);
executor.execute(() -> {
    System.err.println("線程2嘗試獲取lock鎖...");
    lock.lock();
    try {
        System.err.println("線程2拿到鎖並進入try");
        // 喚醒condition的全部線程
        condition.signalAll();
        System.err.println("線程2將condition中的線程喚醒");
    } finally {
        lock.unlock();
        System.err.println("線程2執行完畢,釋放lock鎖");
    }
});

結果圖:

1585749193417

能夠從結果圖中看到,

  當線程調用了condition.await()的時候就被放入了condition中,而且此時將持有的鎖釋放,將本身掛起睡覺等待其餘線程喚醒。因此線程2才能在線程1沒執行完的狀況獲取到了鎖,而且線程2執行完操做以後將線程1喚醒,線程1此時實際上是從新進入同步隊列(隊尾)爭取資源的,若是隊列前方還有線程在等待的話它是不會拿到的,要按照隊列順序獲取,能夠本身在本地創多幾個線程試一下。

  經過這段簡單的代碼以後明顯能夠看到condition具備不錯的靈活性,也就是說提供了更多了選擇性,這也就是跟synchronized不一樣的地方,若是使用synchronized加鎖,那麼Object的喚醒方法只能喚醒所有,或者其中的一個,可是ReentrantLock不一樣,有了condition的幫助,能夠不一樣的線程進行不一樣的分組,而後有選擇的喚醒其中的一組或者其中一組的隨機一個。

5.總結

  ReentrantLock的源碼若是有了AQS的基礎,那麼看起來是不費吹灰之力(開個玩笑,仍是要比吹灰費勁的)。因此本章的篇幅也比較簡單,先從一個例子說明了ReentrantLock的用法, 而且經過這個例子介紹了ReentrantLock可重入、悲觀鎖的幾個特性;接着對其lock方法進行源碼跟蹤,從而瞭解到其內部的方法都是由繼承AQS的內部類Sync來實現的,而Sync又分紅了兩個類,表明兩種不一樣的鎖——公平鎖和非公平鎖;接下來再講到兩種鎖的具體實現和釋放的邏輯,到這裏加鎖解鎖的流程就完整了;最後再介紹ReentrantLock的另外一種特性——Condition,這種特性容許其選擇特定的線程來爭奪鎖,也能夠選擇性的喚醒鎖,到這裏整篇文章就告一段落。

 

孤獨的人不必定是天才,還多是得了鬱抑症。

相關文章
相關標籤/搜索