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