天天看點

徹底搞懂JDK 公平鎖,非公平鎖原理

作者:奮鬥的架構師

前言

jdk中鎖的底層實作是AQS,上一篇文章有講解它的源碼

這篇文章跟大家分享下讀寫鎖,公平鎖,非公平鎖的實作原理。首先他們都是基于AQS實作的。

AQS提供了tryAcquire,tryRelease的方法來供它的子類來實作,不同的鎖通過不同的實作,進而産生了不同的鎖效果。這在設計模式中屬于模闆方法(核心的代碼流程都是一樣的先定義好,不同的類的實作方法表現出不同的效果)。

下面看下他們都是怎麼實作的

非公平鎖實作

首先入口ReentrantLock,它的使用方法這裡不做說明了,很簡單,可以自行網上搜尋。

它的構造方法如下:

ReentrantLock

public ReentrantLock() {
        // 無參構造預設是非公平鎖
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        // 可以在構造方法中指定參數來建立公平鎖還是非公平鎖
        sync = fair ? new FairSync() : new NonfairSync();
    }           

先分析非公平鎖的實作

NonfairSync

/**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            // 原子操作,如果state目前為0,則更新成1。NonfairSync繼承了Sync,Sync繼承AQS,state
            // 是AQS中的一個屬性。如果多個線程同時調用compareAndSetState方法,同一時間肯定隻有一個
            // 成功傳回true,其他的就是進入acquire方法
            if (compareAndSetState(0, 1))
                // 如果狀态設定成功,則設定目前線程為獨占線程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 該方法會先調用tryAcquire方法,如果成功則可以直接執行你的代碼塊,如果失敗則
                // 建立一個Node節點,并且把目前線程作為一個屬性添加到該Node節點上,然後入AQS中的隊列
                //,最後再阻塞目前線程運作。
                acquire(1);
        }

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

上面的注釋寫的很清楚了,如果調用了acquire方法,那麼就會先執行tryAcquire(1)方法,如果該方法傳回true,則表示lock成功了,可以執行你的代碼。如果傳回false,則會入AQS隊列,并阻塞目前線程運作,這塊是标準AQS流程,前面文章已經說過了。這裡隻分析tryAcquire(1)方法。

上面的代碼中可以看到tryAcquire調用了nonfairTryAcquire方法,該方法是它的父類Sync中實作的,如下:

Sync

final boolean nonfairTryAcquire(int acquires) {
            // 擷取目前線程
            final Thread current = Thread.currentThread();
        	// 擷取AQS中的state屬性
            int c = getState();
        	// 判斷該屬性是否為0,如果為0,則進入争搶鎖分支
            if (c == 0) {
                // 通過compareAndSetState方法實作鎖的争搶,因為多個線程執行該方法,同一時間隻有一個會成功。
                // 這個方法是原子的操作。
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果state不等于0,證明鎖已經被加上了,然後再看加鎖的線程和目前線程是不是同一個線程
            else if (current == getExclusiveOwnerThread()) {
                // 如果是同一個線程,則把state屬性的值加1,然後再設定給state屬性。
                // 這裡也展現出了可重入鎖的特性了,同一個線程内可以多次加鎖
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 給state屬性設定新值。
                setState(nextc);
                return true;
            }
        	// 如果不是上面兩種情況則傳回false,然後就是進入AQS隊列,并阻塞目前線程
            return false;
        }           

上面是非公平鎖的加鎖流程,主要流程很簡單,就是調用compareAndSetState方法争搶鎖,搶成功則執行代碼塊,搶失敗,入AQS隊列然後阻塞目前線程運作。

下面分析解鎖代碼

ReentrantLock調用unlock方法如下:

ReentrantLock

public void unlock() {
        sync.release(1);
    }           

release是AQS的是否鎖方法,它會先調用tryRelease方法,如果該方法傳回true則會周遊AQS隊列,找到下一個節點線程來喚醒,進而執行該線程的代碼。如果tryRelease傳回false那麼解鎖失敗,不會喚醒下一個節點線程。這個也是AQS的核心流程。這裡主要分析tryRelease部分代碼。

NonfairSync沒有定義tryRelease方法,是以會調用它的父類Sync中定義的tryRelease方法,如下:

Sync

protected final boolean tryRelease(int releases) {
            // 這裡releases是1,這裡先擷取state的值,然後減1.
            int c = getState() - releases;
            // 這裡主要判斷,調用release的線程一定要跟加鎖的線程是同一個線程,否則抛異常。
            // 這個是合理的,因為加鎖和解鎖是成對出現的,目前線程加鎖了,解鎖的時候也一定是目前線程
            // 調用tryRelease方法。
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果c的值是0,那麼辨別解鎖成功,并請求加鎖線程的值。這裡c有可能不為0的,因為同一個線程
            // 可能會加鎖多次,加鎖一次這個state值就會加1。是以隻有state值減到0的時候,tryRelease才會
            // 傳回true,辨別解鎖成功的。
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }           

總結

當ReentrantLock調用lock方法的時候,會調用NonfairSync的tryAcquire方法,該方法會先調用compareAndSetState進行鎖的争搶,争搶成功state值從0變成1,如果同一個線程再次加鎖那麼state會繼續加1變成2。

當ReentrantLock調用unlock方法的時候,會調用NonfairSync父類Sync的tryRelease方法,該方法會把AQS的state值減1,每調用一次unlock都會減去1。如果同一個線程調用了兩次lock那麼state是2,然後對應的會調用兩次unlock,會減去1兩次,最後會減成0。如果state是0,則tryRelease方法傳回true,然後就會執行AQS後續流程,擷取下一個節點線程去運作。

它是怎麼展現出非公平性的?

主要是在NonfairSync的tryAcquire方法中。該方法會調用compareAndSetState來争搶鎖,比如第一個線程調用lock方法,compareAndSetState傳回true加鎖成功了。然後後續再進來的線程,如果上一個加鎖線程沒有unlock,那進來的線程都會進入AQS隊列排隊。如果加鎖線程調用unlock了。

這時可能會發生鎖的争搶,那兩個線程呢?

即unlock觸發AQS喚醒下一個節點的線程和調用ReentrantLock的lock方法新線程。重新調用lock方法肯定會調用compareAndSetState方法,上一章講AQS中喚醒流程的時候也講解過,喚醒一個節點後,會再次調用tryAcquire方法。是以他們會調用compareAndSetState方法進行鎖的争搶,誰搶成功誰有機會執行,其實這是不公平的,新線程是後來的,AQS中排隊的線程是先到的,按順序新來的線程應該排隊執行的。這就是展現了非公平性。

有了非公平鎖的講解,公平鎖就容易了,下面分析公平鎖。

公平鎖實作

公平鎖是在ReentrantLock中定義FairSync。這裡還是主要分析tryAcquire和tryRelease操作。

加鎖代碼如下:

FairSync

final void lock() {
            // 加鎖流程,調用AQS的核心流程,在acquire會調用tryAcquire方法來實作公平鎖的操作
            acquire(1);
        }

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            // 擷取目前線程
            final Thread current = Thread.currentThread();
        	// 擷取目前AQS的state值
            int c = getState();
            // 如果state值為0,說明還沒有其他線程加鎖
            if (c == 0) {
                // hasQueuedPredecessors方法是檢查隊列中在目前線程前面是不是有其他的節點。如果
                // 隊列前面有其他節點排隊,并且加鎖線程不是目前線程則該方法傳回false,然後AQS就會
                // 把目前線程入隊列,然後阻塞。
                // 如果隊列中沒有其他節點排隊,那麼就會争搶鎖,争搶成功設定目前線程會加鎖線程,然後傳回true
                // 該線程代碼得以執行。
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                // 上面非公平鎖也講過,如果同一個線程多次加鎖,則這裡state值會加1。同時該方法傳回true
                // 目前線程代碼會執行。同一個線程多次加鎖是不會阻塞的,這就是可重入鎖
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 更新state值。
                setState(nextc);
                return true;
            }
            // 否則傳回false
            return false;
        }           

對于公平鎖的解鎖方法和非公平鎖是一樣,都是state值減去1,然後判斷是不是減到0了,如果為0,則喚醒AQS隊列中下一個節點線程執行。

總結

公平鎖tryAcquire和非公平鎖很像,主要就是多了一個hasQueuedPredecessors方法判斷,這也是公平鎖核心所在。公平鎖加鎖的時候會判斷下AQS目前隊列中是否有其他節點,如果有,那麼tryAcquire傳回false,然後目前線程節點會入AQS隊列排隊,等待unlock喚醒執行。

這裡就展現了公平鎖的公平性,新來的線程調用lock的時候,如果隊列中之前有其他線程排隊,那麼它是不會加鎖成功的,它也要排隊等待執行。

下一個文章給大家講解讀寫鎖的實作原理

繼續閱讀