Java volatile與鎖機(jī)制對(duì)比案例分析
前言:最近有強(qiáng)烈的感覺(jué)就是在補(bǔ)充上學(xué)的時(shí)候沒(méi)學(xué)好的知識(shí),沒(méi)打好的基礎(chǔ),果然只要是想做得好,欠過(guò)的債總是要還的。我也沒(méi)有想到工作幾年,竟然會(huì)有這樣悔不當(dāng)初的總結(jié)。
曾經(jīng)的鮮衣怒馬,終究是過(guò)眼云煙,潮水退去,發(fā)現(xiàn)裸泳的竟然是我自己。
多線(xiàn)程案例
一個(gè)經(jīng)典場(chǎng)景:
當(dāng)前類(lèi)中有一個(gè)volatile Strategy strategy作為成員變量,這個(gè)成員變量有不加鎖的getter和setter,類(lèi)還有一個(gè)成員方法execute是對(duì)strategy進(jìn)行某種操作,這個(gè)execute方法是受鎖保護(hù)的。
假設(shè)這個(gè)execute要執(zhí)行10s鐘,在這個(gè)執(zhí)行的過(guò)程中如果getter和setter被其他線(xiàn)程調(diào)用了,getter/setter線(xiàn)程是否會(huì)阻塞?如果getter/setter加鎖,它們的調(diào)用線(xiàn)程會(huì)不會(huì)阻塞?
問(wèn)題的答案:
如果getter/setter不加鎖,非阻塞,它們的調(diào)用線(xiàn)程可以立即執(zhí)行。
如果getter/setter加鎖,阻塞,且此時(shí)volatile關(guān)鍵詞可以去掉。
機(jī)制對(duì)比
鎖 和 volatile 是兩種完全不同的機(jī)制,它們?cè)谶@個(gè)場(chǎng)景下是互不干擾的。
volatile機(jī)制
volatile 保證的是變量的可見(jiàn)性和防止指令重排序。
volatile修飾的變量,可見(jiàn)性是指變量發(fā)生改變時(shí)會(huì)對(duì)其他線(xiàn)程立即可見(jiàn)。也就是說(shuō)volatile的變量發(fā)生寫(xiě)操作時(shí),是立即刷新到主存的,讀操作也是立即從主存讀取最新的值,不會(huì)從緩存中讀。
防止指令重排序是指如果當(dāng)前操作在機(jī)器指令上會(huì)被翻譯成多個(gè)指令,那么這些個(gè)指令不會(huì)被亂序執(zhí)行。
volatile它不涉及互斥性。讀寫(xiě) volatile 變量就像讀寫(xiě)普通變量一樣,不會(huì)導(dǎo)致線(xiàn)程掛起,JVM是直接對(duì)內(nèi)存進(jìn)行操作的。
鎖機(jī)制
在這個(gè)場(chǎng)景下,這里就不展開(kāi)synchronized 和ReentrantLock的區(qū)別了。
鎖保證的是對(duì)內(nèi)存的獨(dú)占訪(fǎng)問(wèn),鎖的概念緊密關(guān)聯(lián)的是對(duì)象。被鎖修飾的代碼邏輯,可以保證原子性,本質(zhì)上是因?yàn)橹挥形ㄒ坏木€(xiàn)程會(huì)訪(fǎng)問(wèn)當(dāng)前的代碼邏輯,那么這段邏輯在其他線(xiàn)程看來(lái)就是“不可再分的”。那么為什么鎖保護(hù)的是對(duì)象,在原子性上卻生效在代碼邏輯上了?鎖通過(guò)鎖住對(duì)象(獨(dú)占訪(fǎng)問(wèn)),實(shí)現(xiàn)了邏輯上排他性地執(zhí)行代碼。因?yàn)閯e人進(jìn)不來(lái),所以無(wú)法打斷你的邏輯,也就無(wú)法看到中間狀態(tài)。
有鎖保護(hù)時(shí),線(xiàn)程對(duì)對(duì)象操作會(huì)獲取當(dāng)前對(duì)象的Monitor(監(jiān)視器鎖)。如果當(dāng)前對(duì)象上鎖,則線(xiàn)程阻塞,這就是鎖的互斥性。
但同時(shí),鎖在釋放時(shí),也會(huì)立即刷新主存,這也就說(shuō)明了鎖也保證了“可見(jiàn)性”,此時(shí)對(duì)象已經(jīng)刷盤(pán)了。根據(jù) Java 內(nèi)存模型,線(xiàn)程釋放鎖時(shí)會(huì)將本地內(nèi)存的修改強(qiáng)制刷新回主存,線(xiàn)程獲取鎖時(shí)會(huì)強(qiáng)制從主存重新加載變量。這就是為什么此時(shí)volatile關(guān)鍵字需要去掉。
多線(xiàn)程訪(fǎng)問(wèn)變量案例解釋
基于以上的理解,再考慮最開(kāi)始的案例。
getter/setter不加鎖
public class StrategyContext {
// 1. volatile 保證可見(jiàn)性,但不提供鎖
private volatile Strategy strategy = new Strategy("Default");
// 2. 無(wú)鎖 Setter:任何時(shí)候都能調(diào),不會(huì)被阻塞
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
// 3. 無(wú)鎖 Getter:任何時(shí)候都能調(diào)
public Strategy getStrategy() {
return strategy;
}
// 4. 加鎖的方法:執(zhí)行耗時(shí) 10 秒
public synchronized void execute() {
System.out.println(Thread.currentThread().getName() + " 拿到鎖,開(kāi)始執(zhí)行 execute (10s)...");
try {
// 模擬長(zhǎng)時(shí)間執(zhí)行
for (int i = 0; i < 5; i++) {
// 危險(xiǎn):如果這里直接使用成員變量 strategy,可能會(huì)在執(zhí)行中途發(fā)現(xiàn)它變了
System.out.println("Execute 中途讀取策略: " + strategy.getName());
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)束,釋放鎖。");
}
}風(fēng)險(xiǎn)點(diǎn):如果線(xiàn)程 A 的 execute 方法邏輯中,在第 1 秒讀取了一次 strategy,在第 9 秒又讀取了一次 strategy。那么線(xiàn)程 A 會(huì)發(fā)現(xiàn)在同一個(gè)方法執(zhí)行過(guò)程中,strategy 變了(前 1 秒是老策略,后 1 秒是新策略)。這可能導(dǎo)致邏輯不一致。
解決方案:內(nèi)存快照。將volatile對(duì)象賦給一個(gè)局部變量,后續(xù)的邏輯使用局部變量操作,那么后續(xù)的邏輯都基于局部變量。注意這里有暫時(shí)的一致性問(wèn)題,即如果當(dāng)前方法需要執(zhí)行10s,而其他線(xiàn)程在這個(gè)過(guò)程中修改了strategy,那么get方法會(huì)拿到最新的值,但是execute會(huì)一直執(zhí)行上一個(gè)快照變量下的邏輯。
public synchronized void execute() {
// 【關(guān)鍵】進(jìn)入方法時(shí),將 volatile 變量賦值給局部變量
// 局部變量存在于線(xiàn)程棧中,其他線(xiàn)程無(wú)法修改它
Strategy localStrategy = this.strategy;
// 后續(xù)所有操作都使用 localStrategy
// 即使成員變量 this.strategy 被其他線(xiàn)程改了,localStrategy 依然指向舊的對(duì)象
localStrategy.algorithm();
}getter/setter加鎖
public class StrategyContext {
private volatile Strategy strategy = new Strategy("Default");
//1. 加鎖,完全互斥,阻塞
public synchronized void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
//2. 加鎖,完全互斥,阻塞
public synchronized Strategy getStrategy() {
return strategy;
}
// 加鎖的方法:執(zhí)行耗時(shí) 10 秒
public synchronized void execute() {
System.out.println(Thread.currentThread().getName() + " 拿到鎖,開(kāi)始執(zhí)行 execute (10s)...");
try {
// 此時(shí)其他線(xiàn)程執(zhí)行g(shù)etter, setter方法會(huì)掛起等待,直到execute方法退出,鎖釋放,JVM喚醒等待線(xiàn)程
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)束,釋放鎖。");
}
}問(wèn)題
- synchronized 本身就保證了可見(jiàn)性。此時(shí)volatile關(guān)鍵詞是多余的。
- 性能低,讀寫(xiě)性能不對(duì)稱(chēng),極端情況讀操作需要掛起等待方法執(zhí)行,但返回引用本身是納秒級(jí)的操作。
- 死鎖風(fēng)險(xiǎn),如果 execute 內(nèi)部還需要獲取其他鎖,持有大鎖的時(shí)間越長(zhǎng),發(fā)生死鎖的概率越高。
解決
當(dāng)然可以用上面那種getter/setter不加鎖的方式解決,弱一致性問(wèn)題用局部快照。本質(zhì)上也是一種copy-on-write的思路。
如果要求強(qiáng)一致性問(wèn)題,可以使用讀寫(xiě)鎖。
ReentrantReadWriteLock 的核心在于把鎖分成了兩把:讀鎖 (Read Lock):共享鎖。大家都可以讀,只要沒(méi)人寫(xiě)。寫(xiě)鎖 (Write Lock):獨(dú)占鎖。只要我在寫(xiě),誰(shuí)都別想讀,也別想寫(xiě)。
對(duì)于加多把鎖的情況,很重要的就是要分清哪里加讀鎖,哪里加寫(xiě)鎖。getter/setter很清楚,而execute呢?這里要結(jié)合Java內(nèi)存模型理解,方法調(diào)用本質(zhì)是讀,上讀鎖。
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RWLockStrategyContext {
private Strategy strategy = new Strategy("Default");
//注意讀寫(xiě)有鎖保護(hù)時(shí),不需要volatile的可見(jiàn)性了
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
// 1. 寫(xiě)鎖保護(hù) Setter
// 只有當(dāng)沒(méi)有任何人在讀(包括 execute 和 get),也沒(méi)有人在寫(xiě)時(shí),才能拿到這把鎖
public void setStrategy(Strategy strategy) {
rwLock.writeLock().lock();
try {
this.strategy = strategy;
} finally {
rwLock.writeLock().unlock();
}
}
// 2. 讀鎖保護(hù) Getter
// 只要沒(méi)有人持有寫(xiě)鎖,可以多線(xiàn)程訪(fǎng)問(wèn)
public Strategy getStrategy() {
rwLock.readLock().lock();
try {
return strategy;
} finally {
rwLock.readLock().unlock();
}
}
// 3. 讀鎖保護(hù) Execute
public void execute() {
rwLock.readLock().lock(); // 獲取讀鎖
try {
System.out.println(Thread.currentThread().getName() + " 拿到讀鎖,開(kāi)始 execute (10s)...");
// 只要還在這個(gè) try 塊里,寫(xiě)鎖(setStrategy)就進(jìn)不來(lái)
// 但是其他讀鎖(getStrategy)可以隨便進(jìn)!
for (int i = 0; i < 5; i++) {
System.out.println("Execute 運(yùn)行中,當(dāng)前策略: " + strategy.getName());
try { Thread.sleep(2000); } catch (InterruptedException e) {}
}
} finally {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)束,釋放讀鎖。");
rwLock.readLock().unlock();
}
}
}并發(fā)模型理解
鎖的本質(zhì)
“鎖”的本質(zhì)是對(duì)象頭的一部分,它與對(duì)象緊密連接,跟著對(duì)象走的。面對(duì)并發(fā)編程的場(chǎng)景,需要牢記的是雖然你總是在代碼邏輯里加鎖,但鎖 鎖住的是共享內(nèi)存,不是線(xiàn)程,不是代碼邏輯。
而編程世界里,“實(shí)體”、“功能” 都是靠代碼邏輯實(shí)現(xiàn)的,叫鎖,并不是物理世界里的真實(shí)屏障,而是一種邏輯機(jī)制。這也解釋了只有上鎖才有互斥性,而鎖和volatile是互不影響的。因?yàn)橹挥写a邏輯執(zhí)行到syncronized/ReentrantLock鎖時(shí),會(huì)觸發(fā)鎖邏輯,去訪(fǎng)問(wèn)對(duì)象頭。查看鎖狀態(tài)。
如果一個(gè)線(xiàn)程受鎖保護(hù),另一個(gè)線(xiàn)程不受鎖保護(hù),那么后者在執(zhí)行的時(shí)候,就不會(huì)走到鎖的機(jī)制里,它會(huì)直接操作內(nèi)存邏輯。
如果一個(gè)線(xiàn)程受鎖保護(hù),另一個(gè)線(xiàn)程受相同的鎖保護(hù),那么后者在執(zhí)行的時(shí)候,就會(huì)走到鎖機(jī)制里,先檢查鎖狀態(tài),再執(zhí)行內(nèi)存邏輯。
Java 內(nèi)存模型
在這里解釋一下為什么execute,方法調(diào)用本質(zhì)是個(gè)讀操作。
相對(duì)于并發(fā)模型而言:代碼的“執(zhí)行”是邏輯行為,“執(zhí)行”本身是不需要鎖的,只有“執(zhí)行過(guò)程中訪(fǎng)問(wèn)共享數(shù)據(jù)”才需要鎖。
strategy.execute() 在 JVM 的指令層面,它實(shí)際上分成了兩步完全獨(dú)立的操作:讀strategy引用+執(zhí)行execute邏輯。
讀/加載:
- 線(xiàn)程找到這個(gè) strategy 這個(gè)變量的在堆內(nèi)存的引用,從內(nèi)存中讀取(拷貝一份地址)到當(dāng)前的棧幀中。volatile保證是從主存讀到的最新值。
執(zhí)行 - 線(xiàn)程拿著剛才讀到的地址,去堆內(nèi)存中找到那個(gè)對(duì)象,在對(duì)象中找到execute方法在方法區(qū)中的地址,然后JVM為這個(gè)方法創(chuàng)建一個(gè)新的棧幀,一行一行把方法區(qū)內(nèi)的指令壓棧執(zhí)行。因此代碼內(nèi)部的調(diào)用在本地棧幀執(zhí)行。棧幀是私有的,安全。
- 這里一旦讀完成了,這一步的執(zhí)行實(shí)際上就跟 變量沒(méi)有任何關(guān)系了。此時(shí)及時(shí)strategy引用變了,也不影響執(zhí)行,因?yàn)榇a執(zhí)行的是strategy被讀取進(jìn)棧幀那一刻的副本。
后記
如果對(duì)volatile和鎖的機(jī)制理解的很透徹,會(huì)發(fā)現(xiàn)最開(kāi)始的的問(wèn)題答案變得非常清晰。
很多東西都是當(dāng)你懂了會(huì)發(fā)現(xiàn)一點(diǎn)都不難,但是如果沒(méi)有理解到位就會(huì)拿不準(zhǔn)。這些是Java并發(fā)編程的純基礎(chǔ)內(nèi)容,但是總會(huì)在不同場(chǎng)景下有新的理解,??闯P?。我其實(shí)知道當(dāng)前的理解還是不夠底層的,比如沒(méi)有涉及到底層的讀寫(xiě)屏障和緩存一致性原則等等,這些東西在我校招入職的時(shí)候,我的導(dǎo)師就發(fā)給我一篇英文經(jīng)典文獻(xiàn)讓我看,我看過(guò),但全忘了,沒(méi)理解透。而現(xiàn)在,我還是沒(méi)有達(dá)到他當(dāng)時(shí)對(duì)我的要求。
到此這篇關(guān)于Java volatile與鎖機(jī)制對(duì)比的文章就介紹到這了,更多相關(guān)Java volatile鎖機(jī)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring-AOP-ProceedingJoinPoint的使用詳解
這篇文章主要介紹了Spring-AOP-ProceedingJoinPoint的使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
SpringBoot啟動(dòng)執(zhí)行sql腳本的3種方法實(shí)例
在應(yīng)用程序啟動(dòng)后,可以自動(dòng)執(zhí)行建庫(kù)、建表等SQL腳本,下面這篇文章主要給大家介紹了關(guān)于SpringBoot啟動(dòng)執(zhí)行sql腳本的3種方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-01-01
Spring AI與DeepSeek實(shí)戰(zhàn)一之快速打造智能對(duì)話(huà)應(yīng)用
本文詳細(xì)介紹了如何通過(guò)SpringAI框架集成DeepSeek大模型,實(shí)現(xiàn)普通對(duì)話(huà)和流式對(duì)話(huà)功能,步驟包括申請(qǐng)API-KEY、項(xiàng)目搭建、配置API-KEY、創(chuàng)建ChatClient對(duì)象、創(chuàng)建對(duì)話(huà)接口、切換模型、使用prompt模板、流式對(duì)話(huà)等,感興趣的朋友一起看看吧2025-03-03
詳解Java中使用泛型實(shí)現(xiàn)快速排序算法的方法
這篇文章主要介紹了Java中使用泛型實(shí)現(xiàn)快速排序算法的方法,快速排序的平均時(shí)間復(fù)雜度為(n\log n),文中的方法立足于基礎(chǔ)而并沒(méi)有考慮優(yōu)化處理,需要的朋友可以參考下2016-05-05
Spring?Boot?基于?CAS?實(shí)現(xiàn)單點(diǎn)登錄的原理、實(shí)踐與優(yōu)化全解析(最新整理)
本文詳解SpringBoot集成CAS單點(diǎn)登錄的原理、實(shí)現(xiàn)步驟及優(yōu)缺點(diǎn),涵蓋CASServer與Client架構(gòu)、票據(jù)機(jī)制、配置方法,并提供優(yōu)化策略如集群部署、緩存加速和用戶(hù)體驗(yàn)提升方案,助力企業(yè)實(shí)現(xiàn)統(tǒng)一認(rèn)證與高效管理,感興趣的朋友一起看看吧2025-07-07
Spring boot如何配置請(qǐng)求的入?yún)⒑统鰠son數(shù)據(jù)格式
這篇文章主要介紹了spring boot如何配置請(qǐng)求的入?yún)⒑统鰠son數(shù)據(jù)格式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
java編程實(shí)現(xiàn)郵件定時(shí)發(fā)送的方法
這篇文章主要介紹了java編程實(shí)現(xiàn)郵件定時(shí)發(fā)送的方法,涉及Java基于定時(shí)器實(shí)現(xiàn)計(jì)劃任務(wù)的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
SpringBoot中緩存@Cacheable出錯(cuò)的問(wèn)題解決
本文主要介紹了SpringBoot中緩存@Cacheable出錯(cuò)的問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-10-10
StringUtils工具包中字符串非空判斷isNotEmpty和isNotBlank的區(qū)別
今天小編就為大家分享一篇關(guān)于StringUtils工具包中字符串非空判斷isNotEmpty和isNotBlank的區(qū)別,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-12-12

