面試突擊:公平鎖和非公平鎖有什么區(qū)別?
作者 | 磊哥
來(lái)源 | Java面試真題解析(ID:aimianshi666)
轉(zhuǎn)載請(qǐng)聯(lián)系授權(quán)(微信ID:GG_Stone)
從公平的角度來(lái)說(shuō),Java 中的鎖總共可分為兩類(lèi):公平鎖和非公平鎖。但公平鎖和非公平鎖有哪些區(qū)別?孰優(yōu)孰劣呢?在 Java 中的應(yīng)用場(chǎng)景又有哪些呢?接下來(lái)我們一起來(lái)看。
正文
公平鎖:每個(gè)線程獲取鎖的順序是按照線程訪問(wèn)鎖的先后順序獲取的,最前面的線程總是最先獲取到鎖。非公平鎖:每個(gè)線程獲取鎖的順序是隨機(jī)的,并不會(huì)遵循先來(lái)先得的規(guī)則,所有線程會(huì)競(jìng)爭(zhēng)獲取鎖。舉個(gè)例子,公平鎖就像開(kāi)車(chē)經(jīng)過(guò)收費(fèi)站一樣,所有的車(chē)都會(huì)排隊(duì)等待通過(guò),先來(lái)的車(chē)先通過(guò),如下圖所示:
通過(guò)收費(fèi)站的順序也是先來(lái)先到,分別是張三、李四、王五,這種情況就是公平鎖。而非公平鎖相當(dāng)于,來(lái)了一個(gè)強(qiáng)行加塞的老司機(jī),它不會(huì)準(zhǔn)守排隊(duì)規(guī)則,來(lái)了之后就會(huì)試圖強(qiáng)行加塞,如果加塞成功就順利通過(guò),當(dāng)然也有可能加塞失敗,如果失敗就乖乖去后面排隊(duì),這種情況就是非公平鎖。
應(yīng)用場(chǎng)景
在 Java 語(yǔ)言中,鎖 synchronized 和 ReentrantLock 默認(rèn)都是非公平鎖,當(dāng)然我們?cè)趧?chuàng)建 ReentrantLock 時(shí),可以手動(dòng)指定其為公平鎖,但 synchronized 只能為非公平鎖。ReentrantLock 默認(rèn)為非公平鎖可以在它的源碼實(shí)現(xiàn)中得到驗(yàn)證,如下源碼所示:
當(dāng)使用 new ReentrantLock(true) 時(shí),可以創(chuàng)建公平鎖,如下源碼所示:
當(dāng)使用 new ReentrantLock(true) 時(shí),可以創(chuàng)建公平鎖,如下源碼所示:
公平和非公平鎖代碼演示
接下來(lái)我們使用 ReentrantLock 來(lái)演示一下公平鎖和非公平鎖的執(zhí)行差異,首先定義一個(gè)公平鎖,開(kāi)啟 3 個(gè)線程,每個(gè)線程執(zhí)行兩次加鎖和釋放鎖并打印線程名的操作,如下代碼所示:
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("當(dāng)前線程:" + Thread.currentThread()
.getName());
lock.unlock();
}
}).start();
}
}
}
以上程序的執(zhí)行結(jié)果如下圖所示:
接下來(lái)我們使用非公平鎖來(lái)執(zhí)行上面的代碼,具體實(shí)現(xiàn)如下:
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("當(dāng)前線程:" + Thread.currentThread()
.getName());
lock.unlock();
}
}).start();
}
}
}
以上程序的執(zhí)行結(jié)果如下圖所示:
從上述結(jié)果可以看出,使用公平鎖線程獲取鎖的順序是:A -> B -> C -> A -> B -> C,也就是按順序獲取鎖。而非公平鎖,獲取鎖的順序是 A -> A -> B -> B -> C -> C,原因是所有線程都爭(zhēng)搶鎖時(shí),因?yàn)楫?dāng)前執(zhí)行線程處于活躍狀態(tài),其他線程屬于等待狀態(tài)(還需要被喚醒),所以當(dāng)前線程總是會(huì)先獲取到鎖,所以最終獲取鎖的順序是:A -> A -> B -> B -> C -> C。
執(zhí)行流程分析
公平鎖執(zhí)行流程
獲取鎖時(shí),先將線程自己添加到等待隊(duì)列的隊(duì)尾并休眠,當(dāng)某線程用完鎖之后,會(huì)去喚醒等待隊(duì)列中隊(duì)首的線程嘗試去獲取鎖,鎖的使用順序也就是隊(duì)列中的先后順序,在整個(gè)過(guò)程中,線程會(huì)從運(yùn)行狀態(tài)切換到休眠狀態(tài),再?gòu)男菝郀顟B(tài)恢復(fù)成運(yùn)行狀態(tài),但線程每次休眠和恢復(fù)都需要從用戶態(tài)轉(zhuǎn)換成內(nèi)核態(tài),而這個(gè)狀態(tài)的轉(zhuǎn)換是比較慢的,所以公平鎖的執(zhí)行速度會(huì)比較慢。
非公平鎖執(zhí)行流程
當(dāng)線程獲取鎖時(shí),會(huì)先通過(guò) CAS 嘗試獲取鎖,如果獲取成功就直接擁有鎖,如果獲取鎖失敗才會(huì)進(jìn)入等待隊(duì)列,等待下次嘗試獲取鎖。這樣做的好處是,獲取鎖不用遵循先到先得的規(guī)則,從而避免了線程休眠和恢復(fù)的操作,這樣就加速了程序的執(zhí)行效率。公平鎖和非公平鎖的性能測(cè)試結(jié)果如下,以下測(cè)試數(shù)據(jù)來(lái)自于《Java并發(fā)編程實(shí)戰(zhàn)》:
從上述結(jié)果可以看出,使用非公平鎖的吞吐率(單位時(shí)間內(nèi)成功獲取鎖的平均速率)要比公平鎖高很多。
優(yōu)缺點(diǎn)分析
公平鎖的優(yōu)點(diǎn)是按序平均分配鎖資源,不會(huì)出現(xiàn)線程餓死的情況,它的缺點(diǎn)是按序喚醒線程的開(kāi)銷(xiāo)大,執(zhí)行性能不高。非公平鎖的優(yōu)點(diǎn)是執(zhí)行效率高,誰(shuí)先獲取到鎖,鎖就屬于誰(shuí),不會(huì)“按資排輩”以及順序喚醒,但缺點(diǎn)是資源分配隨機(jī)性強(qiáng),可能會(huì)出現(xiàn)線程餓死的情況。
總結(jié)
在 Java 語(yǔ)言中,鎖的默認(rèn)實(shí)現(xiàn)都是非公平鎖,原因是非公平鎖的效率更高,使用 ReentrantLock 可以手動(dòng)指定其為公平鎖。非公平鎖注重的是性能,而公平鎖注重的是鎖資源的平均分配,所以我們要選擇合適的場(chǎng)景來(lái)應(yīng)用二者。