排隊打飯:公平鎖和非公平鎖(面試)

簡介


有個小夥伴最近諮詢我,前段時間他被面試官問了synchronized公平鎖還是非公平鎖?當時就蒙圈了,最後面試結果可想而知,今天我們就用一個通俗的案例加上代碼來說明公平鎖非公平鎖。其實公平鎖這個概念是JUC工具包才有的,比如ReentrantLock才有公平鎖的概念,這篇文章我們結合生活中的實例用2段代碼說明ReentrantLock公平鎖和非公平鎖,以及證明synchronized是非公平鎖的。希望對小夥伴有幫助。

公平鎖、非公平鎖概念


  • 公平鎖:舉一個簡單例子,有五個同學每天必須排隊去打飯,為了簡單起見,我們給這五名同學沒人定義一個編號,分別為編號001編號005,這五名同學按先來後到的排隊,打飯,先來的同學能先打到飯。每個同學都是一個線程,在這個過程中後來的同學是不允許插隊的,這就是公平鎖。
  • 非公平鎖:後來到同學不一定後打到飯,在打飯的過程中,是允許插隊的,這種線程插入的行為人們認為是不公平的。舉個例子,比如編號為001,002,003,004的同學先到先排隊了,005最後來排隊本應該排在004後面的,但是005看001正好打完飯離開,他就去插隊了,也就是打飯的順序由001->002->003->004->005變為001->005->002->003->004。其實你現在應該理解了,公平鎖就是正常排隊,非公平就是插隊。當然你可能會有疑問?是不是005插到001的後面一定會成功,答案是不一定,這要看時機的,我們剛才說了「005看001正好打完飯離開」,下面應該是002了,可能打飯阿姨還沒問002準備吃什麼,就看005已經排到前面去了,那005就插隊成功了,這就是時機。下面我們用程序代碼來加深理解。

synchronized非公平鎖


/**
 * @author :jiaolian
 * @date :Created in 2020-12-31 16:01
 * @description:食堂打飯:synchronized不公平
 * @modified By:
 * 公眾號:叫練
 */
public class SyncUnFairLockTest {

    //食堂
    private static class DiningRoom {
        //獲取食物
        public void getFood() {
            System.out.println(Thread.currentThread().getName()+":排隊中");
            synchronized (this) {
                System.out.println(Thread.currentThread().getName()+":@@@@@@打飯中@@@@@@@");
            }
        }
    }

    public static void main(String[] args) {
        DiningRoom diningRoom = new DiningRoom();
        //讓5個同學去打飯
        for (int i=0; i<5; i++) {
            new Thread(()->{
                diningRoom.getFood();
            },"同學編號:00"+(i+1)).start();
        }
    }
}

如上代碼:我們定義一個內部類DiningRoom表示食堂,getFood方法裏面用synchronized鎖修飾this指向DiningRoom的實例對象(22行中的diningRoom對象),主類中讓編號001至005五個同學同時去打飯,用於測試先排隊的同學是否能先打到飯?運行程序得到其中一種執行結果如下圖所示,002->004->001->003->005同學先去排隊,但打飯的順序是002->003->001->004->005,說明這裡003和001兩個同學插隊了,插到004前面了,我們詳細分析執行過程,002先搶到鎖打飯了,釋放了鎖,本來應該是接下來是004搶到鎖去打飯(因為004是比003先來排隊),但003搶到鎖,打飯了,釋放了鎖,這是第一次插隊。現在還是來004搶鎖,但是沒搶到又被001搶到了,釋放鎖後才被004搶到,這是第二次插隊,後面分別再是004->005搶到鎖,釋放鎖,程序執行完畢。因為003和001插隊,我們用代碼證明了synchronized是非公平鎖。緊接着我們來看下ReentrantLock公平鎖和非公平鎖。
image.png

ReentrantLock非公平鎖


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

/**
 * @author :jiaolian
 * @date :Created in 2020-12-31 11:11
 * @description:非公平鎖測試  在獲取鎖的時候和再獲取鎖的順序不一致;
 * @modified By:
 * 公眾號:叫練
 */
public class UnFairLockTest {

    private static final Lock LOCK = new ReentrantLock(false);

    //食堂
    private static class DiningRoom {
        //獲取食物
        public void getFood() {
            try {
                System.out.println(Thread.currentThread().getName()+":正在排隊");
                LOCK.lock();
                System.out.println(Thread.currentThread().getName()+":@@@@@@打飯中@@@@@@@");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        DiningRoom diningRoom = new DiningRoom();
        //讓5個同學去打飯
        for (int i=0; i<5; i++) {
            new Thread(()->{
                diningRoom.getFood();
            },"同學編號:00"+(i+1)).start();
        }
    }
}

如上代碼:我們在代碼第13行中定義了Lock LOCK = new ReentrantLock(false);ReentrantLock的參數是false表示非公平鎖,上面代碼需要用LOCK.lock()加鎖,LOCK.unlock()解鎖,需要放入try,finally代碼塊中,目的是如果try中加鎖後代碼發生異常鎖最終執行LOCK.unlock(),鎖總能被釋放。主類中讓編號001至005五個同學同時去打飯,得到其中一種執行結果如下圖所示,001->004->005->003->002同學先去排隊,但打飯的順序是001->005->004->003->002,這裡005同學插隊了,插到004前面。我們詳細分析執行過程:001先來搶到鎖打飯了並釋放了鎖,接下來本應該是004搶到鎖,因為它先排隊,但005卻在004之前搶到鎖,打飯了,005比004後來,卻先打飯,這就是不公平鎖,後面的執行結果按先來後到執行,程序結束。我們用代碼證明了ReentrantLock是非公平的鎖。緊接着我們來看下ReentrantLock另一種作為公平鎖的情況。
image.png

ReentrantLock公平鎖


基於上面的案例,我們不重複貼代碼了,將上述代碼中13行的private static final Lock LOCK = new ReentrantLock(false);參數由false改為true,private static final Lock LOCK = new ReentrantLock(true);無論執行多少次可以得出一個結論:先排隊的童鞋能先打飯,不允許插對體現的就是公平鎖。
image.png

ReentrantLock底層原理


ReentrantLock是基於AbstractQueuedSynchronizer(抽象隊列同步器,簡稱aqs)實現的,aqs底層維護了一個帶頭的雙向鏈表,用來同步線程,鏈表每個節點用Node表示,每個Node會記錄線程信息,上下節點,節點狀態等信息,aqs控制Node的生命周期。如下圖所示,aqs也包含條件隊列,鎖和條件隊列(condition)是一對多的關係,也就是說一個鎖可以對應多個條件隊列,線程間的通信在條件隊列里通過await,single/singleAll方法控制,synchronized只有一個條件隊列用wait,notify/notifyAll來實現,這裡不展開說了,《母雞下蛋實例:多線程通信生產者和消費者wait/notify和condition/await/signal條件隊列》和《Synchronized用法原理和鎖優化升級過程(面試)》可以看我文章,裏面有大量清晰簡單案例。條件隊列也是以鏈表形式存在。Lock是基於juc包實現,synchronized是本地方法基於c++實現。
image.png

總結


今天用生活中的例子轉化成代碼,詳細的介紹了公平鎖和非公平鎖,並簡單的介紹了aqs實現原理,給您的建議是認真把代碼敲一遍,如果執行了一遍代碼應該能看明白,喜歡的請點贊加關注哦。我是叫練【公眾號】,邊叫邊練。
image.png