面試突擊46:公平鎖和非公平鎖有什麼區別?

從公平的角度來說,Java 中的鎖總共可分為兩類:公平鎖和非公平鎖。但公平鎖和非公平鎖有哪些區別?孰優孰劣呢?在 Java 中的應用場景又有哪些呢?接下來我們一起來看。

正文

公平鎖:每個執行緒獲取鎖的順序是按照執行緒訪問鎖的先後順序獲取的,最前面的執行緒總是最先獲取到鎖。
非公平鎖:每個執行緒獲取鎖的順序是隨機的,並不會遵循先來先得的規則,所有執行緒會競爭獲取鎖。
舉個例子,公平鎖就像開車經過收費站一樣,所有的車都會排隊等待通過,先來的車先通過,如下圖所示:
image.png
通過收費站的順序也是先來先到,分別是張三、李四、王五,這種情況就是公平鎖。
而非公平鎖相當於,來了一個強行加塞的老司機,它不會准守排隊規則,來了之後就會試圖強行加塞,如果加塞成功就順利通過,當然也有可能加塞失敗,如果失敗就乖乖去後面排隊,這種情況就是非公平鎖。
image.png

應用場景

在 Java 語言中,鎖 synchronized 和 ReentrantLock 默認都是非公平鎖,當然我們在創建 ReentrantLock 時,可以手動指定其為公平鎖,但 synchronized 只能為非公平鎖。
ReentrantLock 默認為非公平鎖可以在它的源碼實現中得到驗證,如下源碼所示:

當使用 new ReentrantLock(true) 時,可以創建公平鎖,如下源碼所示:
image.png

公平和非公平鎖程式碼演示

接下來我們使用 ReentrantLock 來演示一下公平鎖和非公平鎖的執行差異,首先定義一個公平鎖,開啟 3 個執行緒,每個執行緒執行兩次加鎖和釋放鎖並列印執行緒名的操作,如下程式碼所示:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockFairTest {
    static Lock lock = new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    lock.lock();
                    System.out.println("當前執行緒:" + Thread.currentThread()
                            .getName());
                    lock.unlock();
                }
            }).start();
        }
    }
}

以上程式的執行結果如下圖所示:
image.png
接下來我們使用非公平鎖來執行上面的程式碼,具體實現如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockFairTest {
    static Lock lock = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 2; j++) {
                    lock.lock();
                    System.out.println("當前執行緒:" + Thread.currentThread()
                            .getName());
                    lock.unlock();
                }
            }).start();
        }
    }
}

以上程式的執行結果如下圖所示:
image.png
從上述結果可以看出,使用公平鎖執行緒獲取鎖的順序是:A -> B -> C -> A -> B -> C,也就是按順序獲取鎖。而非公平鎖,獲取鎖的順序是 A -> A -> B -> B -> C -> C,原因是所有執行緒都爭搶鎖時,因為當前執行執行緒處於活躍狀態,其他執行緒屬於等待狀態(還需要被喚醒),所以當前執行緒總是會先獲取到鎖,所以最終獲取鎖的順序是:A -> A -> B -> B -> C -> C。

執行流程分析

公平鎖執行流程

獲取鎖時,先將執行緒自己添加到等待隊列的隊尾並休眠,當某執行緒用完鎖之後,會去喚醒等待隊列中隊首的執行緒嘗試去獲取鎖,鎖的使用順序也就是隊列中的先後順序,在整個過程中,執行緒會從運行狀態切換到休眠狀態,再從休眠狀態恢復成運行狀態,但執行緒每次休眠和恢復都需要從用戶態轉換成內核態,而這個狀態的轉換是比較慢的,所以公平鎖的執行速度會比較慢。

非公平鎖執行流程

當執行緒獲取鎖時,會先通過 CAS 嘗試獲取鎖,如果獲取成功就直接擁有鎖,如果獲取鎖失敗才會進入等待隊列,等待下次嘗試獲取鎖。這樣做的好處是,獲取鎖不用遵循先到先得的規則,從而避免了執行緒休眠和恢復的操作,這樣就加速了程式的執行效率。
公平鎖和非公平鎖的性能測試結果如下,以下測試數據來自於《Java並發編程實戰》:

image.png
從上述結果可以看出,使用非公平鎖的吞吐率(單位時間內成功獲取鎖的平均速率)要比公平鎖高很多。

優缺點分析

公平鎖的優點是按序平均分配鎖資源,不會出現執行緒餓死的情況,它的缺點是按序喚醒執行緒的開銷大,執行性能不高。
非公平鎖的優點是執行效率高,誰先獲取到鎖,鎖就屬於誰,不會「按資排輩」以及順序喚醒,但缺點是資源分配隨機性強,可能會出現執行緒餓死的情況。

總結

在 Java 語言中,鎖的默認實現都是非公平鎖,原因是非公平鎖的效率更高,使用 ReentrantLock 可以手動指定其為公平鎖。非公平鎖注重的是性能,而公平鎖注重的是鎖資源的平均分配,所以我們要選擇合適的場景來應用二者。

是非審之於己,毀譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集://gitee.com/mydb/interview

Tags: