簡直了,被“Java并發(fā)鎖”問題追問到自閉
故事
地鐵上,小帥雙目空洞地望著窗外...絕望,發(fā)自內(nèi)心地感到絕望...
距離失業(yè)已經(jīng)過去兩個月了,這是小帥接到的第四次面試邀請。“回去等通知吧...”,簡簡單單的六個字,把小帥的心再次打入了冰窖。
上次“【ThreadLocal問出花】”,小帥其實(shí)也有吸取教訓(xùn)得,這次對于多線程的問題還是做了很多準(zhǔn)備的...可是沒想到這次的結(jié)果居然也還是這樣。
“Java中的鎖了解吧?介紹一下吧”,面試官不緊不慢地問到。
“樂觀鎖、悲觀鎖、公平鎖、非公平鎖,然后平時咱們的synchronized是基于.....”小帥把知道的所有關(guān)于鎖的基本都回答了一遍。
面試官對他笑了笑,“就這些嗎?還有呢?比如自旋鎖、可重入鎖、獨(dú)占鎖....并且說一下你的理解,或者聊一下使用場景的優(yōu)劣吧。”
“額.....以前好像看到過...”小帥語無倫次地回答到。
“嗯,行吧,之前的那些答得可以的,不過一會我這邊有個會,要不今天咱們就聊到這里?回去等通知吧...”
Java中讓人眼花繚亂的鎖你是否真的一一清楚了?
試問這樣一個大而寬的問題,大家能夠總結(jié)全嗎,如果讓各位來回答,能否回答完全呢?
我們在實(shí)際的并發(fā)編程中,常常遇到多個線程訪問一個共享變量的情況,當(dāng)同時對共享變量進(jìn)行讀寫操作的時候,就會產(chǎn)生數(shù)據(jù)不一致的情況。為了保證資源獲取的有序性,我們就常常會用到并發(fā)鎖。
那么接下來咱們就來聊聊這些Java并發(fā)鎖的理解吧。我們將從以下這些方面來一起回顧一下Java中的并發(fā)鎖。
概要
樂觀鎖和悲觀鎖:線程是否鎖住同步資源
大家其實(shí)對樂觀鎖和悲觀鎖聽說的比較多一些,所以咱們就先來聊聊這兩種類型的鎖。這兩種類型的鎖,本質(zhì)區(qū)分是要看線程是否鎖住同步資源。
先來看一下悲觀鎖。悲觀鎖就是每次去拿數(shù)據(jù)的時候都會認(rèn)為別人會修改數(shù)據(jù),所以在讀取數(shù)據(jù)的時候都會上鎖。這樣就會導(dǎo)致線程臨時阻塞。
悲觀鎖
再來看一下樂觀鎖,樂觀鎖就是每次在拿數(shù)據(jù)的時候都假設(shè)別人不會修改數(shù)據(jù),所以都不會進(jìn)行上鎖;只有在更新數(shù)據(jù)的時候才去判斷之前有沒有別的線程更新了這條數(shù)據(jù)。如果沒有更新,那么當(dāng)前線程會自己修改數(shù)據(jù)并且寫入成功。如果數(shù)據(jù)已經(jīng)被其他線程更新了,那么會報(bào)錯或者自動重試,例如下圖。
樂觀鎖
上述兩種鎖,并沒有優(yōu)劣之分。只是看相關(guān)的場景然后分別去使用。
- 樂觀鎖:適用于寫少讀多的場景。因?yàn)椴挥蒙湘i,釋放鎖,省去了鎖的開銷,從而提升了吞吐量。
- 悲觀鎖:適用于寫多讀少的場景。因?yàn)榫€程競爭激烈,如果使用樂觀鎖會導(dǎo)致線程不斷進(jìn)行重試,反而降低吞吐量。
共享鎖和獨(dú)占鎖:多個線程是否共享同一把鎖
并發(fā)場景下,如果多個線程能夠共享一把鎖,那么就是所謂的共享鎖,如果不能,那么則為獨(dú)占鎖(其他命名:排他鎖或者獨(dú)享鎖)。
共享鎖指鎖可以被多個線程持有。如果一個線程對數(shù)據(jù)加上共享鎖,那么其他線程只能對數(shù)據(jù)再加共享鎖,不能加獨(dú)占鎖。另外的共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。如下圖。
共享鎖
獨(dú)占鎖是指鎖一次只能被一個線程持有,如果一個線程對數(shù)據(jù)加上獨(dú)占鎖,那么其他的線程則不能對該數(shù)據(jù)再加任何類型的鎖。如果一個線程獲取獨(dú)占鎖,那么則該線程既可以讀數(shù)據(jù)又可以修改數(shù)據(jù)。
獨(dú)占鎖
對于獨(dú)占鎖來說,大家比較熟悉的就是synchronized和J.U.C包中的Lock實(shí)現(xiàn)類。
大家可能也聽說過互斥鎖,其實(shí)互斥鎖就是獨(dú)占鎖的一種常規(guī)實(shí)現(xiàn)。
讀寫鎖是共享鎖的一種具體實(shí)現(xiàn)。讀寫鎖管理一組鎖,一個是只讀的鎖,一個是寫鎖。
讀鎖可以再沒有寫鎖的時候被多個線程同時持有,而寫鎖是獨(dú)占的,于此同時寫鎖的優(yōu)先級要高于讀鎖,一個獲得了讀鎖的線程必須能看到前一個釋放的寫鎖更新的內(nèi)容。
讀寫鎖和互斥鎖對比,其性能更高,每次只有一個寫線程,但是有多個線程可以并發(fā)讀。
讀寫鎖
例如,ReentrantReadWriteLock。具體偽代碼如下:
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 公眾號:程序員老貓
**/
public class ReadWriteLockDemo {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void readData() {
lock.readLock().lock(); // 獲取讀鎖
try {
// 讀取共享數(shù)據(jù)
} finally {
lock.readLock().unlock(); // 釋放讀鎖
}
}
public void writeData() {
lock.writeLock().lock(); // 獲取寫鎖
try {
// 修改或?qū)懭霐?shù)據(jù)
} finally {
lock.writeLock().unlock(); // 釋放寫鎖
}
}
}
公平鎖和非公平鎖:多線程競爭時是否要排隊(duì)
我們根據(jù)多線程在競爭鎖的時候是否需要排隊(duì)從來判斷其鎖的類型是公平鎖還是非公平鎖。
公平鎖指多個線程按照申請鎖的順序來獲取鎖。類似食堂排隊(duì)打飯,先到的可以先打飯。
公平鎖
非公平鎖是指多個線程獲取鎖的順序并不是按照申請鎖的順序進(jìn)行的,有可能后申請的比先申請的優(yōu)先獲得鎖,高并發(fā)場景下,優(yōu)先級就有可能發(fā)生反轉(zhuǎn)。如下圖:
咱們在日常開發(fā)的過程中經(jīng)常用到synchronized,其底層其實(shí)就是非公平鎖。當(dāng)然如果我們要使用公平鎖的情況下,我們也可以使用ReentrantLock。偽代碼如下:
Lock lock = new ReetrantLock(false);
ReentrantLock默認(rèn)為非公平鎖,設(shè)置為true的時候表示公平鎖。當(dāng)設(shè)置為false的時候表示非公平鎖。
可重入鎖和不可重入鎖:同一個線程中多個流程是否能夠獲取同一把鎖。
如果一個線程中的多個流程能夠獲取同一把鎖,那么我們就叫該所為可重入鎖,反之則為不可重入鎖。咱們光看文字描述的話可能比較抽象。我們看一下下圖。
在Java中可重入鎖一般有ReentrantLock,其命名就已經(jīng)很明確了。另外的synchronized也是可重入鎖。可重入鎖的優(yōu)勢是可以一定程度上避免死鎖發(fā)生。上面的示意圖轉(zhuǎn)換為如下demo:
public synchronized void methodA() {
methodB()
}
public synchronized void methodB() {
methodC()
}
public synchronized void methodC(){
doSomeThing()
}
自旋鎖或者自適應(yīng)自旋鎖:線程鎖定同步資源失敗,如該線程沒有被阻塞場景下發(fā)生
如果一個線程鎖住同步資源失敗,但是又希望這個線程不被阻塞,那么此時咱們就可以使用自旋鎖或者自適應(yīng)自旋鎖。自旋鎖指線程沒有獲得鎖的情況下不被掛起,而是執(zhí)行一個忙循環(huán)。那么這個忙循環(huán)的話就成為自旋。如下:
自旋鎖
目的:減少線程被掛起的概率,因?yàn)榫€程被掛起和喚醒也是消費(fèi)資源。
Java中AtomicInteger類就有自旋的操作,如下源代碼:
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
上述方法中weakCompareAndSetInt(),就可以被稱為是CAS操作,如果失敗,那么會一直循環(huán)獲取當(dāng)前的value值然后進(jìn)行重試操作。那么這個過程其實(shí)就是自旋了。
其他分類的鎖
上述我們聊到的這系列的鎖應(yīng)該是大家聽到比較多的。其實(shí)還有其他的分類。在此不做一一展開了,有興趣的小伙伴當(dāng)然也可以深入去了解一下。例如根據(jù)線程競爭同步資源的時候,細(xì)節(jié)流程是否發(fā)生變化,分為偏向鎖、輕量級鎖和重量級鎖。在比如,相信大家對HashMap底層原理倒背如流吧,對ConcurrentHashMap應(yīng)該也有了解,那么ConcurrentHashMap底層其實(shí)將鎖的粒度進(jìn)一步細(xì)化了,存在了分段鎖的概念等等。
總結(jié)
這些讓人眼花繚亂的鎖,如果面試官問到的話,大家是否能夠說出一二呢?相信看完上面的解釋,大家心里多多少少也有數(shù)了吧。當(dāng)然關(guān)于最后一點(diǎn)其他分類的鎖,老貓沒有展開。有興趣的小伙伴可以自行查閱一下這些分類。