面試被問公平鎖和非公平鎖,我竟用“排隊打飯”解釋得清清楚楚 簡介 公平鎖、非公平鎖概念 總結

簡介

有個小夥伴最近諮詢我,前段時間他被面試官問了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公平鎖和非公平鎖。

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

如上代碼:我們在代碼中定義了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另一種作爲公平鎖的情況。

ReentrantLock公平鎖

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

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++實現。

總結

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

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