Java鎖的分類:一文列出Java常見的所有鎖,并分析其實(shí)現(xiàn)原理!
什么是鎖
鎖是一種同步機(jī)制,用于確保多線程訪問共享資源或執(zhí)行代碼塊時(shí)不發(fā)生沖突
目的:防止數(shù)據(jù)不一致或數(shù)據(jù)損壞的問題
鎖的分類
按鎖的獲取方式分類
- 內(nèi)置鎖
通過synchronized關(guān)鍵字實(shí)現(xiàn),JVM自動(dòng)管理鎖的獲取與釋放。
- 每個(gè)對(duì)象關(guān)聯(lián)一個(gè)監(jiān)視器鎖,線程進(jìn)入同步塊時(shí)自動(dòng)獲取。退出時(shí)自動(dòng)釋放。
特點(diǎn):可重入、非公平、自動(dòng)異常處理
底層機(jī)制:依賴對(duì)象頭中的Mark Word記錄鎖狀態(tài) - 顯式鎖
需要手動(dòng)調(diào)用Lock接口的實(shí)現(xiàn)類(例如:ReentrantLock),提供更靈活的控制
- 特點(diǎn):支持公平鎖、可中斷、超時(shí)獲取、支持綁定多個(gè)條件變量
底層機(jī)制:基于AQS(AbstractQueuedSynchronizer)隊(duì)列同步器實(shí)現(xiàn)
按鎖的功能分類
基于多線程能否共享一把鎖,可以把鎖分為共享鎖和互斥鎖
互斥鎖(Mutual Exclusion Lock)
互斥鎖用于控制對(duì)共享資源訪問的一種同步機(jī)制,可以確保在任何給定的時(shí)間點(diǎn)上只有一個(gè)線程可以訪問特定的臨界區(qū)或共享資源。也叫排他鎖。
工作原理:互斥鎖有兩種狀態(tài):鎖定和未鎖定。當(dāng)一個(gè)線程想要訪問受保護(hù)的資源時(shí),必須首先嘗試獲取互斥鎖,如果此時(shí)鎖處于未鎖定狀態(tài),則該線程可以所得鎖并繼續(xù)執(zhí)行;如果鎖已經(jīng)被其他線程持有,則當(dāng)前線程會(huì)被阻塞直到鎖釋放。
互斥鎖的特點(diǎn):
- 原子性:獲取和釋放鎖的過程必須是原子性的,這些操作不能被打斷
- 唯一性:在同一時(shí)間點(diǎn)上,只有一個(gè)線程可以持有互斥鎖
- 可重入性:某些實(shí)現(xiàn)允許同一個(gè)線程多次獲取同一把鎖,但是必須保證每次獲取都對(duì)應(yīng)一次釋放
- 不可剝奪性:一旦一個(gè)線程獲得了互斥鎖,除非該線程主動(dòng)釋放鎖,否則其他線程無法強(qiáng)制剝奪這個(gè)鎖
基于互斥鎖的特點(diǎn),可以避免競(jìng)態(tài)條件和數(shù)據(jù)不一致問題
常見的問題
- 死鎖:兩個(gè)或多個(gè)線程互相等待對(duì)方持有的資源,從而陷入永久等待狀態(tài)
- 饑餓:某些線程永遠(yuǎn)得不到執(zhí)行機(jī)會(huì),因?yàn)槠渌€程總是優(yōu)先獲取所需的資源
共享鎖(Shared Lock)
共享鎖也就是讀鎖,是允許多線程共享訪問共享資源的鎖機(jī)制,主要應(yīng)用于讀多寫少的場(chǎng)景。通過區(qū)分讀寫操作,顯著提升并發(fā)性能。
Java中實(shí)現(xiàn)共享鎖的底層原理:【依托于AQS框架】
狀態(tài)管理:AQS使用一個(gè)32位的int變量表示鎖狀態(tài),其中高16位記錄讀鎖的持有數(shù)量,低16位記錄寫鎖的持有數(shù)量。通過CAS操作修改state的值,確保線程安全。
讀鎖和寫鎖共享同一個(gè)CLH同步隊(duì)列,AQS通過CLH完成同步狀態(tài)的管理,若當(dāng)前線程獲取同步狀態(tài)失敗時(shí),AQS則會(huì)將當(dāng)前線程的狀態(tài)信息構(gòu)造成一個(gè)Node節(jié)點(diǎn),添加到CLH隊(duì)列中,并且阻塞當(dāng)前線程;當(dāng)同步狀態(tài)釋放時(shí),會(huì)把首節(jié)點(diǎn)喚醒,使其再次嘗試獲取同步狀態(tài)
什么是CLH同步隊(duì)列?
AQS原理&CLH同步隊(duì)列
讀鎖的特點(diǎn):
- 共享性:允許多個(gè)線程同時(shí)持有讀鎖
- 可重入性:同一線程可重復(fù)獲取讀鎖
- 與寫鎖互斥:如果當(dāng)前線程持有寫鎖,則讀鎖必須等待
public class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
// 讀操作(共享鎖)
public V get(K key) {
readLock.lock();
try {
return map.get(key);
} finally {
readLock.unlock();
}
}
// 寫操作(排他鎖)
public void put(K key, V value) {
writeLock.lock();
try {
map.put(key, value);
} finally {
writeLock.unlock();
}
}
}
寫鎖降級(jí)為讀鎖
鎖降級(jí)是指在持有寫鎖的情況下獲取讀鎖,隨后釋放寫鎖的過程
核心邏輯:
- 持有寫鎖:確保當(dāng)前線程獨(dú)占資源,其他線程無法讀寫
- 獲取讀鎖:在未釋放寫鎖時(shí)獲取讀鎖,防止其他線程獲取寫鎖修改數(shù)據(jù)
- 釋放寫鎖:降級(jí)為讀鎖后,允許其他線程讀取數(shù)據(jù),但是不能寫入數(shù)據(jù)
- 釋放讀鎖:完成讀操作后釋放讀鎖,允許其他線程獲取寫鎖
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
writeLock.lock(); // 1. 獲取寫鎖
try {
// 修改共享數(shù)據(jù)
readLock.lock(); // 2. 獲取讀鎖
} finally {
writeLock.unlock(); // 3. 釋放寫鎖(此時(shí)仍持有讀鎖)
}
try {
// 讀取數(shù)據(jù)(其他線程可并發(fā)讀,但無法寫)
} finally {
readLock.unlock(); // 4. 釋放讀鎖
}
為什么不支持鎖升級(jí)
鎖升級(jí)是指從讀鎖升級(jí)到寫鎖,Java中并不支持這種操作
可能產(chǎn)生的問題:
- 死鎖風(fēng)險(xiǎn):如果多個(gè)持有讀鎖的線程同時(shí)嘗試升級(jí)為寫鎖,會(huì)互相等待對(duì)方釋放讀鎖,這樣機(jī)會(huì)造成死鎖。
- 競(jìng)爭復(fù)雜性:因?yàn)樽x鎖是可重入,并且支持多個(gè)線程同時(shí)持有的,當(dāng)升級(jí)為寫鎖就會(huì)造成長時(shí)間的阻塞,等待釋放所有讀鎖
說完互斥鎖與共享鎖,接下來從線程需不需要鎖住同步資源的角度,又分為悲觀鎖和樂觀鎖
樂觀鎖(Optimistic Lock)
樂觀認(rèn)為并發(fā)沖突概率低,操作時(shí)不加鎖,只在提交時(shí)檢查數(shù)據(jù)是否被修改
實(shí)現(xiàn)方式:CAS算法和StampedLock
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 內(nèi)部通過CAS自旋實(shí)現(xiàn)
StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// 讀取共享數(shù)據(jù)...
if (!lock.validate(stamp)) {
// 數(shù)據(jù)被修改,轉(zhuǎn)為悲觀讀鎖
stamp = lock.readLock();
// 重新讀取數(shù)據(jù)...
lock.unlockRead(stamp);
}
image.png
悲觀鎖(Pessimistic Lock)
悲觀認(rèn)為并發(fā)沖突概率高,每次訪問共享資源時(shí),先加鎖再操作
實(shí)現(xiàn)方式:synchronized關(guān)鍵字和Lock實(shí)現(xiàn)類
image.png
適用場(chǎng)景:
悲觀鎖適合寫操作多的場(chǎng)景,先加鎖保證寫操作時(shí)數(shù)據(jù)正確
樂觀鎖適合讀操作多的場(chǎng)景,不加鎖的特點(diǎn)能夠大幅度提升性能
根據(jù)等待鎖的方式可分為自旋鎖和阻塞鎖
自旋鎖(Spin Lock)
當(dāng)線程嘗試獲取鎖失敗時(shí),不會(huì)立即放棄CPU,而是通過忙循環(huán)(自旋)不斷嘗試獲取鎖,直到成功為止【默認(rèn)是循環(huán)10次】
實(shí)現(xiàn)自旋鎖的兩種方式
- 使用原子類的CAS實(shí)現(xiàn)自旋鎖--循環(huán)次數(shù)自行控制
public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
while (!locked.compareAndSet(false, true)) {
// 自旋等待(空循環(huán)或短暫休眠)
}
}
public void unlock() {
locked.set(false);
}
}
- 使用synchronized,--循環(huán)次數(shù)默認(rèn)10次,并可以使用
-XX:PreBlockSpin
修改該值
如果沒有成功獲得鎖就會(huì)將該線程掛起
自旋鎖存在的問題:
如果線程鎖在線程自旋剛結(jié)束就釋放掉鎖,那么這個(gè)線程切換上下文的代價(jià)是無端的浪費(fèi)--引出了自適應(yīng)自旋鎖
適應(yīng)型自旋鎖:自旋的次數(shù)或時(shí)間不再固定,而是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來決定
- 如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲取過鎖,并且持有鎖的線程正在運(yùn)行中,JVM會(huì)認(rèn)為該鎖自旋獲取到鎖的可能性很大,會(huì)自動(dòng)增加等待時(shí)間
- 如果對(duì)于某個(gè)鎖,自旋很少成功獲取鎖,那就會(huì)減少自旋時(shí)間或直接不自旋了,避免浪費(fèi)處理器資源
這樣的方式來實(shí)現(xiàn)動(dòng)態(tài)預(yù)測(cè)自旋
image.png
阻塞鎖(Blocking Lock)
阻塞鎖是指當(dāng)線程獲取不到鎖時(shí),不發(fā)生自旋,直接阻塞。
優(yōu)點(diǎn):避免CPU空轉(zhuǎn),節(jié)省資源
缺點(diǎn):線程切換上下文帶來的額外開銷,響應(yīng)延遲較高
根據(jù)多個(gè)線程獲取同一把鎖時(shí)是否先到先得,分為公平鎖和非公平鎖
公平鎖(Fair Lock)
公平鎖是指線程獲取鎖的順序是按照線程請(qǐng)求鎖的時(shí)間順序來決定的【先到先得】
實(shí)現(xiàn)方式:ReentrantLock(true)
底層機(jī)制:底層維護(hù)了一個(gè)FIFO隊(duì)列,記錄等待鎖的線程;鎖釋放時(shí),優(yōu)先喚醒隊(duì)列中的第一個(gè)線程。
image.png
非公平鎖(Non-Fair Lock)
非公平鎖是指線程獲取鎖的順序根據(jù)實(shí)際的競(jìng)爭結(jié)果【誰搶到算誰的】
實(shí)現(xiàn)方式:ReentrantLock(false)或synchronized關(guān)鍵字
底層機(jī)制:新線程嘗試獲取鎖時(shí),直接競(jìng)爭,若競(jìng)爭失敗再進(jìn)入隊(duì)列等待
圖片
公平鎖與非公平鎖的區(qū)別
- 公平鎖:會(huì)將所有想要獲取鎖的線程放入FIFO,這就必然會(huì)在釋放鎖時(shí),觸發(fā)喚醒線程操作
- 非公平鎖:會(huì)直接獲取鎖,若獲取成功,則直接占用資源,無需喚醒;而獲取失敗才會(huì)到隊(duì)列排序,當(dāng)釋放鎖時(shí),觸發(fā)喚醒線程操作
所以非公平鎖插隊(duì)失敗就是公平鎖【優(yōu)先使用非公平鎖】
根據(jù)一把鎖能否重復(fù)獲取同一把鎖,分為可重入鎖和非可重入鎖
可重入鎖(Reentrant Lock)
可重入鎖也叫遞歸鎖,是指同一個(gè)線程再外層方法獲取鎖之后,再進(jìn)入該線程的內(nèi)層方法時(shí)會(huì)自動(dòng)獲取鎖
前提條件:鎖對(duì)象得是同一個(gè)對(duì)象或者Class
實(shí)現(xiàn)方式:ReentrantLock和synchronized都是可重入鎖
ReentrantLock lock = new ReentrantLock();
public void recursiveMethod(int n) {
lock.lock();
try {
if (n > 0) {
recursiveMethod(n - 1); // 遞歸調(diào)用仍可獲取鎖
}
} finally {
lock.unlock();
}
}
public synchronized void methodA() {
methodB(); // 同一線程可直接進(jìn)入另一個(gè)同步方法
}
public synchronized void methodB() {}
底層原理:每次獲取鎖時(shí),內(nèi)部維護(hù)一個(gè)計(jì)數(shù)器,只要釋放鎖(unlock)就對(duì)應(yīng)減少計(jì)數(shù)器,反之增加,直到計(jì)數(shù)器歸零后完全釋放鎖
不會(huì)因?yàn)橹矮@取過沒釋放而阻塞
image.png
非可重入鎖(Non-Reentrant Lock)
禁止同一線程重復(fù)獲取同一把鎖,若嘗試重復(fù)獲取,線程會(huì)立即阻塞或拋出異常
實(shí)現(xiàn)原理:
- 鎖狀態(tài)標(biāo)記:僅記錄鎖是否被占用,不跟蹤持有者
- 重復(fù)獲取行為:若線程已持有鎖,再次調(diào)用lock()會(huì)阻塞或失敗
image.png
根據(jù)在等待鎖的過程中是否可以中斷,分為可中斷鎖與不可中斷鎖
中斷鎖
線程在等待鎖時(shí)可響應(yīng)中斷
java中的中斷鎖:tryLock(time) 和 lockInterruptibly()
ReentrantLock lock = new ReentrantLock();
// 線程A獲取鎖并長期持有(同上)
new Thread(() -> {
lock.lock();
try {
Thread.sleep(10000); // 模擬長時(shí)間持有鎖
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
// 線程B嘗試獲取鎖(可中斷)
Thread threadB = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中斷的鎖請(qǐng)求
try {
System.out.println("ThreadB獲取鎖成功");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("ThreadB被中斷,退出等待");
}
});
threadB.start();
// 主線程嘗試中斷線程B
Thread.sleep(1000);
threadB.interrupt(); // 成功中斷線程B
ReentrantLock.lockInterruptibly()底層機(jī)制:通過AQS的acquireInterruptibly()
方法實(shí)現(xiàn),內(nèi)部調(diào)用doAcquireInterruptibly()
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// AQS的acquireInterruptibly方法
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg); // 支持中斷的等待
}
不可中斷鎖(Non-Interruptible Lock)
一旦線程開始請(qǐng)求,就會(huì)一直阻塞等待,直到獲取鎖
java中的不可中斷鎖:synchronized和lock()
ReentrantLock lock = new ReentrantLock();
// 線程A獲取鎖并長期持有
new Thread(() -> {
lock.lock();
try {
Thread.sleep(10000); // 模擬長時(shí)間持有鎖
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
// 線程B嘗試獲取鎖(不可中斷)
Thread threadB = new Thread(() -> {
lock.lock(); // 不可中斷的鎖請(qǐng)求
try {
System.out.println("ThreadB獲取鎖成功");
} finally {
lock.unlock();
}
});
threadB.start();
// 主線程嘗試中斷線程B
Thread.sleep(1000);
threadB.interrupt(); // 無法中斷線程B的等待
synchronized底層機(jī)制:JVM內(nèi)置鎖機(jī)制,等待鎖的線程進(jìn)入BLOCKED
狀態(tài),不響應(yīng)中斷
ReentrantLock.lock()底層機(jī)制:基于AQS框架,使用不可中斷模式加入等待隊(duì)列
使用場(chǎng)景:
- 不可中斷鎖適用場(chǎng)景
任務(wù)必須完成:如數(shù)據(jù)庫事務(wù)提交、關(guān)鍵數(shù)據(jù)寫入,不允許中途放棄。
簡單同步需求:使用synchronized
快速實(shí)現(xiàn)線程安全。
- 可中斷鎖適用場(chǎng)景
- 高響應(yīng)性要求:如用戶取消操作、服務(wù)超時(shí)控制。
- 復(fù)雜任務(wù)管理:線程池任務(wù)調(diào)度、需要優(yōu)雅終止的后臺(tái)服務(wù)。