京東二面:Java 中一共有 N 種實(shí)現(xiàn)鎖的方式,你知道都有哪些嗎?
首先,我們先來(lái)看下線程安全性的定義,為什么需要鎖?
線程安全,即在多線程編程中,一個(gè)程序或者代碼段在并發(fā)訪問(wèn)時(shí),能夠正確地保持其預(yù)期的行為和狀態(tài),而不會(huì)出現(xiàn)意外的錯(cuò)誤或者不一致的結(jié)果。
而解決線程安全問(wèn)題,主要分為兩大類(lèi):1、無(wú)鎖;2、有鎖。
無(wú)鎖的方式有:
- 局部變量;
- 對(duì)象加 final 為不可變對(duì)象;
- 使用 ThreadLocal 作為線程副本對(duì)象;
- CAS,Compare-And-Swap 即比較并交換,是 Java 十分常見(jiàn)的無(wú)鎖實(shí)現(xiàn)方式。
小白:那有鎖的方式呢,怎么通過(guò)加鎖保證線程安全呢?
別急哈,下面聽(tīng)我給你一一道來(lái)。
Java 有哪些鎖?
從加鎖的策略看,分為隱式鎖和顯示鎖。隱式鎖通過(guò) Synchronized 實(shí)現(xiàn),顯示鎖通過(guò) Lock 實(shí)現(xiàn)。
- 樂(lè)觀鎖:顧名思義,它是一種基于樂(lè)觀的思想,認(rèn)為讀取的數(shù)據(jù)一般不會(huì)沖突,不會(huì)對(duì)其加鎖,而是在最后提交數(shù)據(jù)更新時(shí)判斷數(shù)據(jù)是否被更新,如果沖突,則更新不成功。
- 悲觀鎖:它總是假設(shè)最壞的情況,每次讀取數(shù)據(jù)都認(rèn)為別人會(huì)更新,所以每次讀取數(shù)據(jù)的時(shí)候都會(huì)加鎖,這樣別人就得阻塞等待它處理完釋放鎖后才能去讀取。
樂(lè)觀鎖實(shí)現(xiàn):CAS,比較并交換,通常指的是這樣一種原子操作:針對(duì)一個(gè)變量,首先比較它的內(nèi)存值與某個(gè)期望值是否相同,如果相同,就給它賦一個(gè)新值。
但是,這一篇我們主要來(lái)看下悲觀鎖的一些常用實(shí)現(xiàn)。
syncroized 是什么?
syncronized 是 Java 中的一個(gè)關(guān)鍵字,用于控制對(duì)共享資源的并發(fā)訪問(wèn),從而防止多個(gè)線程同時(shí)訪問(wèn)某個(gè)特定資源,這被稱(chēng)為同步。這個(gè)關(guān)鍵字可以用來(lái)修飾方法或代碼塊。
syncronized 使用對(duì)象鎖保證臨界區(qū)內(nèi)代碼的原子性
圖片
小白:synchronized 的底層原理是什么呀,怎么自己就完成加鎖釋放鎖操作了?
其實(shí) synchronized 的原理也不難,主要有以下兩個(gè)關(guān)鍵點(diǎn)。
- synchronized 又被稱(chēng)為監(jiān)視器鎖,基于 Monitor 機(jī)制實(shí)現(xiàn)的,主要依賴(lài)底層操作系統(tǒng)的互斥原語(yǔ) Mutex(互斥量)。Monitor 類(lèi)比加了鎖的房間,一次只能有一個(gè)線程進(jìn)入,進(jìn)入房間即持有 Monitor,退出后就釋放 Monitor。
- 另一個(gè)關(guān)鍵點(diǎn)是 Java 對(duì)象頭,在 JVM 虛擬機(jī)中,對(duì)象在內(nèi)存中的存儲(chǔ)結(jié)構(gòu)有三部分:對(duì)象頭;實(shí)例數(shù)據(jù);對(duì)齊填充。
對(duì)象頭主要包括標(biāo)記字段 Mark World,元數(shù)據(jù)指針,如果是數(shù)組對(duì)象的話,對(duì)象頭還必須存儲(chǔ)數(shù)組長(zhǎng)度。
圖片
synchronized 也是基于此,通過(guò)鎖對(duì)象的 monitor 獲取和 monitor 釋放來(lái)實(shí)現(xiàn),對(duì)象頭標(biāo)記為存儲(chǔ)具體鎖狀態(tài),ThreadId 記錄持有偏向鎖的線程 ID。
這里,又引申另外出一個(gè)問(wèn)題:你知道什么是偏向鎖呢?
小白:不知道,啥玩意?
synchronized 鎖升級(jí)過(guò)程
說(shuō)到這里,那就不得不提及 synchronized 的鎖升級(jí)機(jī)制了,因?yàn)?synchronized 的加鎖釋放鎖操作會(huì)使得 CPU 在內(nèi)核態(tài)和戶(hù)態(tài)之間發(fā)生切換,有一定性能開(kāi)銷(xiāo)。在 JDK1.5 版本以后,對(duì) synchronized 做了鎖升級(jí)的優(yōu)化,主要利用輕量級(jí)鎖、偏向鎖、自適應(yīng)鎖等減少鎖操作帶來(lái)的開(kāi)銷(xiāo),對(duì)其性能做了很大提升。
圖片
- 無(wú)鎖:沒(méi)有對(duì)資源進(jìn)行加鎖
- 偏向鎖:在大部分情況下,只有一個(gè)線程訪問(wèn)修改資源,該線程自動(dòng)獲取鎖,降低了鎖操作的代價(jià),這里就通過(guò)對(duì)象頭的 ThreadId 記錄線程 ID。
- 輕量級(jí)鎖:當(dāng)前持有偏向鎖,當(dāng)有另外的線程來(lái)訪問(wèn)后,偏向鎖會(huì)升級(jí)為輕量級(jí)鎖,別的線程通過(guò)自旋形式嘗試獲取鎖,不會(huì)阻塞,以提高性能。
- 重量級(jí)鎖:在自旋次數(shù)或時(shí)間超過(guò)一定閾值時(shí),最后會(huì)升級(jí)為重量級(jí)鎖。
小白:哦哦原來(lái)如此,那剛剛你說(shuō)了 Java 除了隱式鎖之外,還有顯示鎖呢?
ReentrantLock 簡(jiǎn)介
在 Java 中,除了對(duì)象鎖,還有顯示的加鎖的方式,比如 Lock 接口,用得比較多的就是 ReentrantLock。它的特性如下:
圖片
下面我們?cè)賮?lái)對(duì)比看下 ReentrantLock 和 synchronized 的區(qū)別
圖片
從這些對(duì)比就能看出 ReentrantLock 使用更加的靈活,特性更加豐富。
ReentrantLock 是一個(gè)悲觀鎖,即是同一個(gè)時(shí)刻,只允許一個(gè)線程訪問(wèn)代碼塊,這一點(diǎn) synchronized 其實(shí)也一樣。
圖片
小白:這個(gè)是挺好用的,但是我們有一些讀多寫(xiě)少的場(chǎng)景中比如緩存,大部分時(shí)間都是讀操作,這里每個(gè)操作都要加鎖,讀性能不是很差嗎,有沒(méi)有更好的方案實(shí)現(xiàn)這種場(chǎng)景呀?
當(dāng)然有的,比如 ReentrantReadWriteLock,讀寫(xiě)鎖。
ReentrantReadWriteLock 介紹
針對(duì)上述場(chǎng)景,Java 提供了讀寫(xiě)鎖 ReentrantReadWriteLock,它的內(nèi)部維護(hù)了一對(duì)相關(guān)的鎖,一個(gè)用于只讀操作,稱(chēng)為讀鎖;一個(gè)用于寫(xiě)入操作,稱(chēng)為寫(xiě)鎖。
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;使用核心代碼如下:
public class LocalCacheService {
static Map<String, Object> localCache = new HashMap<>();
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
static Lock readL = lock.readLock();
static Lock writeL = lock.writeLock();
public static Object read(String key) {
readL.lock();
try {
return localCache.get(key);
} finally {
readL.unlock();
}
}
public static Object save(String key, String value) {
writeL.lock();
try {
return localCache.put(key, value);
} finally {
writeL.unlock();
}
}
}在 ReentrantReadWriteLock 中,多個(gè)線程可以同時(shí)讀取一個(gè)共享資源。
當(dāng)有其他線程的寫(xiě)鎖時(shí),讀線程會(huì)被阻塞,反之一樣。
圖片
讀寫(xiě)鎖設(shè)計(jì)思路
這里有一個(gè)關(guān)鍵點(diǎn),就是在 ReentrantLock 中,使用 AQS 的 state 表示同步狀態(tài),表示鎖被一個(gè)線程重復(fù)獲取的次數(shù)。但是在讀寫(xiě)鎖 ReentrantReadWriteLock 中,如何用一個(gè)變量維護(hù)這兩個(gè)狀態(tài)呢?
實(shí)際 ReentrantReadWriteLock 采用“高低位切割”的方式來(lái)維護(hù),將 state 切分為兩部分:高 16 位表示讀;低 16 位表示寫(xiě)。
分割之后,通過(guò)位運(yùn)算,假設(shè)當(dāng)前狀態(tài)為 S,那么:
- 寫(xiě)狀態(tài)=S&0x0000FFFF(將高 16 位全部移除),當(dāng)寫(xiě)狀態(tài)需要加 1,S+1 再運(yùn)算即可。
- 讀狀態(tài)=S>>>16(無(wú)符號(hào)補(bǔ) 0 右移 16 位),當(dāng)讀狀態(tài)需要加 1,計(jì)算 S+(1<<16)。
圖片
這時(shí),我們?cè)賮?lái)思考下,如果有線程正在讀,寫(xiě)線程需要等待讀線程釋放鎖才能獲取鎖,也就是讀的時(shí)候不允許寫(xiě),那么有沒(méi)有更好的方式改進(jìn)呢?
小白:emm,這個(gè)真的難倒我了。。。。。。
什么是 StampedLock?
哈哈莫慌,Java8 已經(jīng)引入了新的讀寫(xiě)鎖,StampedLock。它和 ReentrantReadWriteLock 相比,區(qū)別在于讀過(guò)程允許獲取寫(xiě)鎖寫(xiě)入,在原來(lái)讀寫(xiě)鎖的基礎(chǔ)上加了一種樂(lè)觀鎖機(jī)制,該模式不會(huì)阻塞寫(xiě)鎖,只是最后會(huì)對(duì)比原來(lái)的值,有著更高的并發(fā)性能。
StampedLock 三種模式如下:
- 獨(dú)占鎖:和 ReentrantReadWriteLock 一樣,同一時(shí)刻只能有一個(gè)寫(xiě)線程獲取資源
圖片
- 悲觀讀鎖:允許多個(gè)線程獲取讀鎖,但是讀寫(xiě)互斥。
圖片
- 樂(lè)觀讀:沒(méi)有加鎖,允許多個(gè)線程獲取樂(lè)觀讀和讀鎖,同時(shí)允許一個(gè)寫(xiě)線程獲取寫(xiě)鎖。
圖片
小白:那這里可以允許多個(gè)讀操作和也給寫(xiě)線程同時(shí)進(jìn)入共享資源操作,那讀取的數(shù)據(jù)被改了怎么辦啊??
別擔(dān)心,樂(lè)觀讀不能保證讀到的數(shù)據(jù)是最新的,所以當(dāng)把數(shù)據(jù)讀取到局部變量的時(shí)候需要通過(guò) lock.validate 方法來(lái)校驗(yàn)是否被修改過(guò),如果是改過(guò)了那么就加上悲觀讀鎖,再重新讀取數(shù)據(jù)到局部變量。































