1. 程式人生 > >排隊打飯:公平鎖和非公平鎖(面試)

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

## 簡介 --- 有個小夥伴最近諮詢我,前段時間他被**面試官**問了**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非公平鎖 --- ```java /** * @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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1609558037089-a8d85d00-541d-477e-a058-585519c45135.png#align=left&display=inline&height=325&margin=%5Bobject%20Object%5D&name=image.png&originHeight=325&originWidth=520&size=22638&status=done&style=none&width=520)
## ReentrantLock非公平鎖 --- ```java 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1609671078884-5e0d3fb2-5054-4ad0-b8cc-ec639015eb23.png#align=left&display=inline&height=304&margin=%5Bobject%20Object%5D&name=image.png&originHeight=304&originWidth=489&size=21239&status=done&style=none&width=489)
## ReentrantLock公平鎖 --- 基於上面的案例,我們不重複貼程式碼了,將上述程式碼中13行的**private static final Lock LOCK = new ReentrantLock(false);**引數由false改為true,**private static final Lock LOCK = new ReentrantLock(true);**無論執行多少次可以得出一個結論:先排隊的童鞋能先打飯,不允許插對體現的就是公平鎖。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1609671757852-a3fcece7-612a-4a93-b62b-d8005d457f7c.png#align=left&display=inline&height=299&margin=%5Bobject%20Object%5D&name=image.png&originHeight=299&originWidth=483&size=21080&status=done&style=none&width=483) ## 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](https://cdn.nlark.com/yuque/0/2021/png/1897706/1609675273851-78ef156d-faf2-4b96-bb25-9559526d9f46.png#align=left&display=inline&height=614&margin=%5Bobject%20Object%5D&name=image.png&originHeight=614&originWidth=654&size=55619&status=done&style=none&width=654) ## 總結 --- 今天用生活中的例子轉化成程式碼,詳細的介紹了公平鎖和非公平鎖,並簡單的介紹了aqs實現原理,給您的建議是認真把程式碼敲一遍,如果執行了一遍程式碼應該能看明白,喜歡的請點贊加關注哦。我是**叫練【公眾號】**,邊叫邊練。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1897706/1609675476019-96edeae4-996d-47df-86ae-90ebb73659b7.png#align=left&display=inline&height=255&margin=%5Bobject%20Object%5D&name=image.png&originHeight=255&originWidth=255&size=76822&status=done&style=none&width=2