天天看點

ReentrantLock源碼解析

談到多線程,就不避開鎖(Lock),jdk中已經為我們提供了好幾種鎖的實作,已經足以滿足我們大部分的需求了,今天我們就來看下最常用的ReentrantLock的實作。

其實最開始是想寫一篇關于StampedLock的源碼分析的,但發現寫StampedLock前避不開ReentrantReadWriteLock,寫ReentrantReadWriteLock又避不開ReentrantLock,他們仨是逐層遞進的關系。ReentrantReadWriteLock解決了一些ReentrantLock無法解決的問題,StampedLock又彌補了ReentrantReadWriteLock的一些不足,三者有各自的設計和有缺點,這篇文章先和你一起看下ReentrantLock,之後我們會再一起去了解ReentrantReadWriteLock和StampedLock,相信有了ReentrantLock的基礎後面的内容也會容易了解很多。

ReentrantLock源碼解析
相對于jdk中很多其他的類來說,ReentrantLock提供的接口已經算是非常簡單,事實上它隻有一個構造參數

boolean fair

,用來指定是公平鎖還是非公平鎖,如果你指定的話預設是非公平鎖。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }           

什麼是公平?這裡的的公平是指每個線程可以有公平的機會擷取到這把鎖,10個線程競争這把鎖,某個線程各有10%的機會擷取到鎖。聽起來很理想主義,但大多數時候不建議使用公平鎖,因為局部性的存在,每個線程對鎖的真正需求度是不同的,有些線程就是需要很頻繁的占有鎖,有些偶爾占有就行。如果你單純是為了公平而導緻供需不平衡,可能有些線程會浪費鎖的持有時間,而有些線程急需用鎖但遲遲擷取不到,導緻線程饑餓,最終導緻整個系統的性能不是最大化的。

最大化鎖的使用率和代碼性能就成了鎖設計最重要的目标。試想如果我們提前知道每個線程對鎖的需求度,然後按需求度給他們配置設定鎖的占有機會,這樣必然能達到鎖的最優使用率。但實際上對于jdk的開發者來說,他哪知道你要拿鎖去做啥、要開幾個線程?是以ReentrantReadWriteLock的設計者用一種很簡單粗暴的方式解決了大部分的問題,我們直接上源碼。

ReentrantLock中最核心的就是Sync的實作,它預設已經實作了非公平鎖的功能,是以你會看到NonfairSync隻是簡簡單單繼承了Sync而已。而Sync的主要功能還是繼承了AbstractQueuedSynchronizer(AQS)。

AbstractQueuedSynchronizer 簡單來說就是維護了一個狀态 __state__,和一個等待線程隊列(一個雙向連結清單),然後通過VarHandle提供的CAS操作保證線程安全。當你在調用lock()的時候,會直接調用到AbstractQueuedSynchronizer中的acquire(int arg)

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

acquire也很簡單,嘗試去擷取鎖,如果擷取失敗,就把目前線程加到等待隊列裡(排隊),并把目前線程的狀态設定為可中斷。

回到ReentrantLock,我們來開下它是如何依賴Sync來實作非公平鎖的。NonfairSync在執行tryAcquire(int arg)的時候,實際執行的是以下代碼。

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {   //如果state是0,說明目前沒有線程持有鎖,用CAS更新狀态,如果CAS成功,就在鎖中寫入目前線程的資訊。  
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  //如果state不是0,也不一定擷取鎖失敗,要看下持有鎖的線程是不是自己,如果是更新state 
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }           

從這可以看出來ReentrantLock是可重入鎖,state的目的就是為了記錄目前鎖被同一個線程擷取了幾次。但是看完這段代碼你肯定沒看出來哪 __不公平了__。别急,我們來對比下公平鎖的實作就知道了。

static final class FairSync extends Sync {
       private static final long serialVersionUID = -3000897897090466540L;
       @ReservedStackAccess
       protected final boolean tryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               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;
       }
   }           

FairSync也是繼承自Sync,但它重寫了加鎖tryAcquire方法,打眼一看和上面非公平鎖的tryAcquire非常像,唯一不同之處就是在state為0時多了個!hasQueuedPredecessors()的判斷。hasQueuedPredecessors()方法是判斷是否有線程在等待去擷取這把鎖,如果有其他線程這次就算是擷取鎖失敗了。

來個易懂的例子,現在辦公室隻有一間衛生間,很多個同僚共用,有人在用衛生間的時候也不會希望别人跑進來和他一起用。公平鎖的實作方式就是我來上衛生間,發現衛生間沒人用,但有人在排隊等衛生間(可能是玩手機沒注意衛生間空了),我隻能乖乖排隊。非公平鎖的實作方式是,我來上衛生間,發現衛生間是空的,不管有沒有人排隊我都占了,這樣顯然對其他排隊的人來說是不公平的。

這種方式在現實世界看起來是非常不合理的,但是如果換種視角,可能越着急的人才是越需要用衛生間的人(可能他拉肚子),讓排隊的人多等會無所謂,這樣才能最大化衛生間的的價值。雖然拉肚子在現實世界不常見,在在計算機中以納秒計的世界裡,有些線程就是比其他線程急很多的情況非常常見,非公平的方式就很合情。再從機率的角度看,如果有個線程需要以更高的頻次使用這把鎖,不排隊去擷取鎖能舍得鎖被擷取到的次數最大化,也很合理。是以非公平鎖合情合理。但曆史告訴我們,凡事沒有絕對,還是需要具體問題具體分析,有些情況下,非公平鎖會導緻線程饑餓。

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

unlock()調用了sync中的release(),release()是繼承自AQS,跳到AQS中就會發現又調用了tryRelease()。ReentrantLock重寫了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;
        }           

釋放鎖的過程是先判斷是否是鎖持有線程,然後更新鎖狀态。如果你進到

setExclusiveOwnerThread(null)

setState(c)

裡面,就會發現這裡沒有用到CAS,會不會出現線程安全的問題?仔細想想其實不會有線程安全的問題,

if (Thread.currentThread() != getExclusiveOwnerThread())

判斷了是目前線程是否持有鎖,保證後續邏輯隻有持有鎖的線程才會執行到,因為之前擷取鎖是用CAS保證線程安全的,是以後面的邏輯也一定是線程安全的。

ReentrantLock源碼解析

除了加鎖和釋放鎖外,ReentrantLock還提供了和鎖、線程相關的的接口,如上圖,從函數名就可以看出其作用了,而且實作代碼比較簡單,這裡就不再贅述了,有興趣可以自行檢視源碼。