最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Java volatile與鎖機(jī)制對比案例分析

 更新時間:2025年12月15日 09:23:35   作者:代碼N年歸來仍是新手村成員  
文章主要介紹了Java中的volatile和鎖機(jī)制,并通過一個多線程案例來對比它們的優(yōu)缺點(diǎn),感興趣的朋友跟隨小編一起看看吧

前言:最近有強(qiáng)烈的感覺就是在補(bǔ)充上學(xué)的時候沒學(xué)好的知識,沒打好的基礎(chǔ),果然只要是想做得好,欠過的債總是要還的。我也沒有想到工作幾年,竟然會有這樣悔不當(dāng)初的總結(jié)。
曾經(jīng)的鮮衣怒馬,終究是過眼云煙,潮水退去,發(fā)現(xiàn)裸泳的竟然是我自己。

多線程案例

一個經(jīng)典場景
當(dāng)前類中有一個volatile Strategy strategy作為成員變量,這個成員變量有不加鎖的getter和setter,類還有一個成員方法execute是對strategy進(jìn)行某種操作,這個execute方法是受鎖保護(hù)的。
假設(shè)這個execute要執(zhí)行10s鐘,在這個執(zhí)行的過程中如果getter和setter被其他線程調(diào)用了,getter/setter線程是否會阻塞?如果getter/setter加鎖,它們的調(diào)用線程會不會阻塞?

問題的答案
如果getter/setter不加鎖,非阻塞,它們的調(diào)用線程可以立即執(zhí)行。
如果getter/setter加鎖,阻塞,且此時volatile關(guān)鍵詞可以去掉。

機(jī)制對比

鎖 和 volatile 是兩種完全不同的機(jī)制,它們在這個場景下是互不干擾的。

volatile機(jī)制

volatile 保證的是變量的可見性和防止指令重排序。
volatile修飾的變量,可見性是指變量發(fā)生改變時會對其他線程立即可見。也就是說volatile的變量發(fā)生寫操作時,是立即刷新到主存的,讀操作也是立即從主存讀取最新的值,不會從緩存中讀。
防止指令重排序是指如果當(dāng)前操作在機(jī)器指令上會被翻譯成多個指令,那么這些個指令不會被亂序執(zhí)行。

volatile它不涉及互斥性。讀寫 volatile 變量就像讀寫普通變量一樣,不會導(dǎo)致線程掛起,JVM是直接對內(nèi)存進(jìn)行操作的。

鎖機(jī)制

在這個場景下,這里就不展開synchronized 和ReentrantLock的區(qū)別了。

鎖保證的是對內(nèi)存的獨(dú)占訪問,鎖的概念緊密關(guān)聯(lián)的是對象。被鎖修飾的代碼邏輯,可以保證原子性,本質(zhì)上是因為只有唯一的線程會訪問當(dāng)前的代碼邏輯,那么這段邏輯在其他線程看來就是“不可再分的”。那么為什么鎖保護(hù)的是對象,在原子性上卻生效在代碼邏輯上了?鎖通過鎖住對象(獨(dú)占訪問),實現(xiàn)了邏輯上排他性地執(zhí)行代碼。因為別人進(jìn)不來,所以無法打斷你的邏輯,也就無法看到中間狀態(tài)。

有鎖保護(hù)時,線程對對象操作會獲取當(dāng)前對象的Monitor(監(jiān)視器鎖)。如果當(dāng)前對象上鎖,則線程阻塞,這就是鎖的互斥性。

但同時,鎖在釋放時,也會立即刷新主存,這也就說明了鎖也保證了“可見性”,此時對象已經(jīng)刷盤了。根據(jù) Java 內(nèi)存模型,線程釋放鎖時會將本地內(nèi)存的修改強(qiáng)制刷新回主存,線程獲取鎖時會強(qiáng)制從主存重新加載變量。這就是為什么此時volatile關(guān)鍵字需要去掉。

多線程訪問變量案例解釋

基于以上的理解,再考慮最開始的案例。

getter/setter不加鎖

public class StrategyContext {
    // 1. volatile 保證可見性,但不提供鎖
    private volatile Strategy strategy = new Strategy("Default");
    // 2. 無鎖 Setter:任何時候都能調(diào),不會被阻塞
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    // 3. 無鎖 Getter:任何時候都能調(diào)
    public Strategy getStrategy() {
        return strategy;
    }
    // 4. 加鎖的方法:執(zhí)行耗時 10 秒
    public synchronized void execute() {
        System.out.println(Thread.currentThread().getName() + " 拿到鎖,開始執(zhí)行 execute (10s)...");
        try {
            // 模擬長時間執(zhí)行
            for (int i = 0; i < 5; i++) {
                // 危險:如果這里直接使用成員變量 strategy,可能會在執(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)險點(diǎn):如果線程 A 的 execute 方法邏輯中,在第 1 秒讀取了一次 strategy,在第 9 秒又讀取了一次 strategy。那么線程 A 會發(fā)現(xiàn)在同一個方法執(zhí)行過程中,strategy 變了(前 1 秒是老策略,后 1 秒是新策略)。這可能導(dǎo)致邏輯不一致。
解決方案:內(nèi)存快照。將volatile對象賦給一個局部變量,后續(xù)的邏輯使用局部變量操作,那么后續(xù)的邏輯都基于局部變量。注意這里有暫時的一致性問題,即如果當(dāng)前方法需要執(zhí)行10s,而其他線程在這個過程中修改了strategy,那么get方法會拿到最新的值,但是execute會一直執(zhí)行上一個快照變量下的邏輯。

public synchronized void execute() {
    // 【關(guān)鍵】進(jìn)入方法時,將 volatile 變量賦值給局部變量
    // 局部變量存在于線程棧中,其他線程無法修改它
    Strategy localStrategy = this.strategy; 
    // 后續(xù)所有操作都使用 localStrategy
    // 即使成員變量 this.strategy 被其他線程改了,localStrategy 依然指向舊的對象
    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í)行耗時 10 秒
    public synchronized void execute() {
        System.out.println(Thread.currentThread().getName() + " 拿到鎖,開始執(zhí)行 execute (10s)...");
        try {
            // 此時其他線程執(zhí)行g(shù)etter, setter方法會掛起等待,直到execute方法退出,鎖釋放,JVM喚醒等待線程
                Thread.sleep(10000); 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 執(zhí)行結(jié)束,釋放鎖。");
    }
}

問題

  1. synchronized 本身就保證了可見性。此時volatile關(guān)鍵詞是多余的。
  2. 性能低,讀寫性能不對稱,極端情況讀操作需要掛起等待方法執(zhí)行,但返回引用本身是納秒級的操作。
  3. 死鎖風(fēng)險,如果 execute 內(nèi)部還需要獲取其他鎖,持有大鎖的時間越長,發(fā)生死鎖的概率越高。

解決
當(dāng)然可以用上面那種getter/setter不加鎖的方式解決,弱一致性問題用局部快照。本質(zhì)上也是一種copy-on-write的思路。
如果要求強(qiáng)一致性問題,可以使用讀寫鎖。
ReentrantReadWriteLock 的核心在于把鎖分成了兩把:讀鎖 (Read Lock):共享鎖。大家都可以讀,只要沒人寫。寫鎖 (Write Lock):獨(dú)占鎖。只要我在寫,誰都別想讀,也別想寫。

對于加多把鎖的情況,很重要的就是要分清哪里加讀鎖,哪里加寫鎖。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");
    //注意讀寫有鎖保護(hù)時,不需要volatile的可見性了
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    // 1. 寫鎖保護(hù) Setter
    // 只有當(dāng)沒有任何人在讀(包括 execute 和 get),也沒有人在寫時,才能拿到這把鎖
    public void setStrategy(Strategy strategy) {
        rwLock.writeLock().lock();
        try {
            this.strategy = strategy;
        } finally {
            rwLock.writeLock().unlock(); 
        }
    }
    // 2. 讀鎖保護(hù) Getter
    // 只要沒有人持有寫鎖,可以多線程訪問
    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() + " 拿到讀鎖,開始 execute (10s)...");
            // 只要還在這個 try 塊里,寫鎖(setStrategy)就進(jìn)不來
            // 但是其他讀鎖(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ì)是對象頭的一部分,它與對象緊密連接,跟著對象走的。面對并發(fā)編程的場景,需要牢記的是雖然你總是在代碼邏輯里加鎖,但鎖 鎖住的是共享內(nèi)存,不是線程,不是代碼邏輯。

而編程世界里,“實體”、“功能” 都是靠代碼邏輯實現(xiàn)的,叫鎖,并不是物理世界里的真實屏障,而是一種邏輯機(jī)制。這也解釋了只有上鎖才有互斥性,而鎖和volatile是互不影響的。因為只有代碼邏輯執(zhí)行到syncronized/ReentrantLock鎖時,會觸發(fā)鎖邏輯,去訪問對象頭。查看鎖狀態(tài)。

如果一個線程受鎖保護(hù),另一個線程不受鎖保護(hù),那么后者在執(zhí)行的時候,就不會走到鎖的機(jī)制里,它會直接操作內(nèi)存邏輯。
如果一個線程受鎖保護(hù),另一個線程受相同的鎖保護(hù),那么后者在執(zhí)行的時候,就會走到鎖機(jī)制里,先檢查鎖狀態(tài),再執(zhí)行內(nèi)存邏輯。

Java 內(nèi)存模型

在這里解釋一下為什么execute,方法調(diào)用本質(zhì)是個讀操作。
相對于并發(fā)模型而言:代碼的“執(zhí)行”是邏輯行為,“執(zhí)行”本身是不需要鎖的,只有“執(zhí)行過程中訪問共享數(shù)據(jù)”才需要鎖。

strategy.execute() 在 JVM 的指令層面,它實際上分成了兩步完全獨(dú)立的操作:讀strategy引用+執(zhí)行execute邏輯。
讀/加載

  • 線程找到這個 strategy 這個變量的在堆內(nèi)存的引用,從內(nèi)存中讀?。截愐环莸刂罚┑疆?dāng)前的棧幀中。volatile保證是從主存讀到的最新值。
    執(zhí)行
  • 線程拿著剛才讀到的地址,去堆內(nèi)存中找到那個對象,在對象中找到execute方法在方法區(qū)中的地址,然后JVM為這個方法創(chuàng)建一個新的棧幀,一行一行把方法區(qū)內(nèi)的指令壓棧執(zhí)行。因此代碼內(nèi)部的調(diào)用在本地棧幀執(zhí)行。棧幀是私有的,安全。
  • 這里一旦讀完成了,這一步的執(zhí)行實際上就跟 變量沒有任何關(guān)系了。此時及時strategy引用變了,也不影響執(zhí)行,因為代碼執(zhí)行的是strategy被讀取進(jìn)棧幀那一刻的副本。

后記

如果對volatile和鎖的機(jī)制理解的很透徹,會發(fā)現(xiàn)最開始的的問題答案變得非常清晰。

很多東西都是當(dāng)你懂了會發(fā)現(xiàn)一點(diǎn)都不難,但是如果沒有理解到位就會拿不準(zhǔn)。這些是Java并發(fā)編程的純基礎(chǔ)內(nèi)容,但是總會在不同場景下有新的理解,??闯P?。我其實知道當(dāng)前的理解還是不夠底層的,比如沒有涉及到底層的讀寫屏障和緩存一致性原則等等,這些東西在我校招入職的時候,我的導(dǎo)師就發(fā)給我一篇英文經(jīng)典文獻(xiàn)讓我看,我看過,但全忘了,沒理解透。而現(xiàn)在,我還是沒有達(dá)到他當(dāng)時對我的要求。

到此這篇關(guān)于Java volatile與鎖機(jī)制對比的文章就介紹到這了,更多相關(guān)Java volatile鎖機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring原生Rpc六種的正確打開方式實現(xiàn)示例

    Spring原生Rpc六種的正確打開方式實現(xiàn)示例

    這篇文章主要為大家展示了Spring原生Rpc六種的正確打開方式實現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助祝大家多多進(jìn)步早日升職加薪
    2022-02-02
  • Spring Boot如何使用AOP實例解析

    Spring Boot如何使用AOP實例解析

    這篇文章主要介紹了Spring Boot如何使用AOP實例解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-04-04
  • IDEA JetBrains Mono字體介紹和安裝教程(詳解)

    IDEA JetBrains Mono字體介紹和安裝教程(詳解)

    這篇文章主要介紹了IDEA JetBrains Mono字體介紹和安裝教程,本給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2020-03-03
  • Spring Boot 注解體系與工程實踐示例指南

    Spring Boot 注解體系與工程實踐示例指南

    本文詳細(xì)介紹了SpringBoot核心注解的語法、語義、適用場景及組合方式,涵蓋了自動配置、條件化注入、AOP與事務(wù)等關(guān)鍵領(lǐng)域,本文通過實例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2025-11-11
  • Maven 搭建SpringMVC+Hibernate項目詳解

    Maven 搭建SpringMVC+Hibernate項目詳解

    本文主要介紹Maven 搭建SpringMVC+Hibernate的知識,這里整理了詳細(xì)的資料,并附示例代碼,有興趣的小伙伴可以參考下
    2016-09-09
  • 一文帶你了解FastExcel的使用

    一文帶你了解FastExcel的使用

    我們知道?EasyExcel?在作者從阿里離職之后就停止維護(hù)了,但在前兩周?EasyExcel?原作者推出了他的升級版框架?FastExcel,下面小編就來和大家聊聊FastExcel的具體使用吧
    2024-12-12
  • 分布式Netty源碼分析概覽

    分布式Netty源碼分析概覽

    這篇文章主要為大家介紹了分布式Netty源碼分析概覽,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-03-03
  • Java多線程中Callable和Future的解讀

    Java多線程中Callable和Future的解讀

    這篇文章主要介紹了Java多線程中Callable和Future的解讀,Callable接口類似于Runnable,從名字就可以看出來了,但是Runnable不會返回結(jié)果,并且無法拋出返回結(jié)果的異常,而Callable功能更強(qiáng)大一些,被線程執(zhí)行后,可以返回值,這個返回值可以被Future拿到,需要的朋友可以參考下
    2023-09-09
  • Java集合系列之HashMap源碼分析

    Java集合系列之HashMap源碼分析

    這篇文章主要為大家詳細(xì)介紹了Java集合系列之HashMap源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-02-02
  • java實現(xiàn)短信驗證碼5分鐘有效時間

    java實現(xiàn)短信驗證碼5分鐘有效時間

    這篇文章主要為大家詳細(xì)介紹了java實現(xiàn)短信驗證碼5分鐘有效時間,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-07-07

最新評論

平和县| 汉阴县| 榆林市| 台中市| 武强县| 芷江| 鹤峰县| 马关县| 阿拉善右旗| 定陶县| 铜川市| 东阿县| 芮城县| 海兴县| 贡嘎县| 东阳市| 柏乡县| 巨鹿县| 宿迁市| 明水县| 堆龙德庆县| 黄骅市| 阳西县| 清流县| 兴山县| 东阿县| 武威市| 沙湾县| 淮北市| 丰原市| 开远市| 曲沃县| 山阴县| 历史| 中西区| 宜君县| 高密市| 南和县| 佳木斯市| 金乡县| 临湘市|