關於公平鎖和非公平鎖的理解

這是看完你就明白的鎖系列的第四篇文章

文章一覽請看這裏

看完你就應該能明白的悲觀鎖和樂觀鎖

看完你就明白的鎖系列之自旋鎖

看完你就明白的鎖系列之鎖的狀態

此篇文章我們來探討一下什麼是鎖的公平性

鎖的公平性與非公平性

我們知道,在併發環境中,多個線程需要對同一資源進行訪問,同一時刻只能有一個線程能夠獲取到鎖並進行資源訪問,那麼剩下的這些線程怎麼辦呢?這就好比食堂排隊打飯的模型,最先到達食堂的人擁有最先買飯的權利,那麼剩下的人就需要在第一個人後面排隊,這是理想的情況,即每個人都能夠買上飯。那麼現實情況是,在你排隊的過程中,就有個別不老實的人想走捷徑,插隊打飯,如果插隊的這個人後面沒有人制止他這種行爲,他就能夠順利買上飯,如果有人制止,他就也得去隊伍後面排隊。

對於正常排隊的人來說,沒有人插隊,每個人都在等待排隊打飯的機會,那麼這種方式對每個人來說都是公平的,先來後到嘛。這種鎖也叫做公平鎖。

file

那麼假如插隊的這個人成功買上飯並且在買飯的過程不管有沒有人制止他,他的這種行爲對正常排隊的人來說都是不公平的,這在鎖的世界中也叫做非公平鎖。

file

file

那麼我們根據上面的描述可以得出下面的結論

公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來分配的,即先來先得的FIFO先進先出順序。而非公平鎖就是一種獲取鎖的搶佔機制,是隨機獲得鎖的,和公平鎖不一樣的就是先來的不一定先得到鎖,這個方式可能造成某些線程一直拿不到鎖,結果也就是不公平的了。

鎖公平性的實現

在 Java 中,我們一般通過 ReetrantLock 來實現鎖的公平性

我們分別通過兩個例子來講解一下鎖的公平性和非公平性

鎖的公平性

public class MyFairLock extends Thread{

    private ReentrantLock lock = new ReentrantLock(true);
    public void fairLock(){
        try {
            lock.lock();
            System.out.println(Thread.currentThread().getName()  + "正在持有鎖");
        }finally {
            System.out.println(Thread.currentThread().getName()  + "釋放了鎖");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        MyFairLock myFairLock = new MyFairLock();
        Runnable runnable = () -> {
            System.out.println(Thread.currentThread().getName() + "啓動");
            myFairLock.fairLock();
        };
        Thread[] thread = new Thread[10];
        for(int i = 0;i < 10;i++){
            thread[i] = new Thread(runnable);
        }
        for(int i = 0;i < 10;i++){
            thread[i].start();
        }
    }
}

我們創建了一個 ReetrantLock,並給構造函數傳了一個 true,我們可以查看 ReetrantLock 的構造函數

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

根據 JavaDoc 的註釋可知,如果是 true 的話,那麼就會創建一個 ReentrantLock 的公平鎖,然後並創建一個 FairSync ,FairSync 其實是一個 Sync 的內部類,它的主要作用是同步對象以獲取公平鎖。

file

而 Sync 是 ReentrantLock 中的內部類,Sync 繼承 AbstractQueuedSynchronizer 類,AbstractQueuedSynchronizer 就是我們常說的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一個類,通過它來實現獨佔鎖和共享鎖。

abstract static class Sync extends AbstractQueuedSynchronizer {...}

也就是說,我們把 fair 參數設置爲 true 之後,就可以實現一個公平鎖了,是這樣嗎?我們回到示例代碼,我們可以執行一下這段代碼,它的輸出是順序獲取的(礙於篇幅的原因,這裏就暫不貼出了),也就是說我們創建了一個公平鎖

鎖的非公平性

與公平性相對的就是非公平性,我們通過設置 fair 參數爲 true,便實現了一個公平鎖,與之相對的,我們把 fair 參數設置爲 false,是不是就是非公平鎖了?用事實證明一下

private ReentrantLock lock = new ReentrantLock(false);

其他代碼不變,我們執行一下看看輸出(部分輸出)

Thread-1啓動
Thread-4啓動
Thread-1正在持有鎖
Thread-1釋放了鎖
Thread-5啓動
Thread-6啓動
Thread-3啓動
Thread-7啓動
Thread-2啓動

可以看到,線程的啓動並沒有按順序獲取,可以看出非公平鎖對鎖的獲取是亂序的,即有一個搶佔鎖的過程。也就是說,我們把 fair 參數設置爲 false 便實現了一個非公平鎖。

公平鎖的原理

接下來,我們通過 ReentrantLock 源碼來講解公平鎖和非公平鎖。首先先來認識一下 ReentrantLock 是什麼

ReentrantLock 基本概述

ReentrantLock 是一把可重入鎖,也是一把互斥鎖,它具有與 synchronized 相同的方法和監視器鎖的語義,但是它比 synchronized 有更多可擴展的功能。

ReentrantLock 的可重入性是指它可以由上次成功鎖定但還未解鎖的線程擁有。當只有一個線程嘗試加鎖時,該線程調用 lock() 方法會立刻返回成功並直接獲取鎖。如果當前線程已經擁有這把鎖,這個方法會立刻返回。可以使用 isHeldByCurrentThreadgetHoldCount 進行檢查。

這個類的構造函數接受可選擇的 fairness 參數,當 fairness 設置爲 true 時,在多線程爭奪嘗試加鎖時,鎖傾向於對等待時間最長的線程訪問,這也是公平性的一種體現。否則,鎖不能保證每個線程的訪問順序,也就是非公平鎖。與使用默認設置的程序相比,使用許多線程訪問的公平鎖的程序可能會顯示較低的總體吞吐量(即較慢;通常要慢得多)。但是獲取鎖並保證線程不會飢餓的次數比較小。無論如何請注意:鎖的公平性不能保證線程調度的公平性。因此,使用公平鎖的多線程之一可能會連續多次獲得它,而其他活動線程沒有進行且當前未持有該鎖。這也是互斥性 的一種體現。

也要注意的 tryLock() 方法不支持公平性。如果鎖是可以獲取的,那麼即使其他線程等待,它仍然能夠返回成功。

推薦使用下面的代碼來進行加鎖和解鎖

class MyFairLock {
  private final ReentrantLock lock = new ReentrantLock();

  public void m() {
    lock.lock();  
    try {
      // ... 
    } finally {
      lock.unlock()
    }
  }
}

ReentrantLock 鎖通過同一線程最多支持2147483647個遞歸鎖。 嘗試超過此限制會導致鎖定方法引發錯誤。

ReentrantLock 如何實現鎖公平性

我們在上面的簡述中提到,ReentrantLock 是可以實現鎖的公平性的,那麼原理是什麼呢?下面我們通過其源碼來了解一下 ReentrantLock 是如何實現鎖的公平性的

跟蹤其源碼發現,調用 Lock.lock() 方法其實是調用了 sync 的內部的方法

abstract void lock();

而 sync 是最基礎的同步控制 Lock 的類,它有公平鎖和非公平鎖的實現。它繼承 AbstractQueuedSynchronizer 即 使用 AQS 狀態代表鎖持有的數量。

lock 是抽象方法是需要被子類實現的,而繼承了 AQS 的類主要有

file

我們可以看到,所有實現了 AQS 的類都位於 JUC 包下,主要有五類:ReentrantLockReentrantReadWriteLockSemaphoreCountDownLatchThreadPoolExecutor,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以實現公平鎖和非公平鎖。

下面是公平鎖 FairSync 的繼承關係

file

非公平鎖的NonFairSync 的繼承關係

file

由繼承圖可以看到,兩個類的繼承關係都是相同的,我們從源碼發現,公平鎖和非公平鎖的實現就是下面這段代碼的區別(下一篇文章我們會從原理角度分析一下公平鎖和非公平鎖的實現)

file

通過上圖中的源代碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()

hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用來 查詢是否有任何線程在等待獲取鎖的時間比當前線程長,也就是說每個等待線程都是在一個隊列中,此方法就是判斷隊列中在當前線程獲取鎖時,是否有等待鎖時間比自己還長的隊列,如果當前線程之前有排隊的線程,返回 true,如果當前線程位於隊列的開頭或隊列爲空,返回 false。

綜上,公平鎖就是通過同步隊列來實現多個線程按照申請鎖的順序來獲取鎖,從而實現公平的特性。非公平鎖加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,所以存在後申請卻先獲得鎖的情況。

文章參考:

https://tech.meituan.com/2018...

https://www.jianshu.com/p/eae...

https://blog.csdn.net/oChangW...

ReentrantLock(重入鎖)功能詳解和應用演示

ReentrantLock(重入鎖)功能詳解和應用演示

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