欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JAVA內(nèi)存模型(JMM)詳解

 更新時(shí)間:2022年12月07日 08:36:36   作者:JAVA旭陽(yáng)  
這篇文章主要介紹了JAVA內(nèi)存模型(JMM)詳解的相關(guān)資料,需要的朋友可以參考下

前言

開(kāi)篇一個(gè)例子,我看看都有誰(shuí)會(huì)?如果不會(huì)的,或者不知道原理的,還是老老實(shí)實(shí)看完這篇文章吧。

@Slf4j(topic = "c.VolatileTest")
public class VolatileTest {
    
    static boolean run = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (run) {
                // do other things
            }
            
            // ?????? 這行會(huì)打印嗎?
            log.info("done .....");
        });
        t.start();
        
        Thread.sleep(1000);

       // 設(shè)置run = false
        run = false;
    }
}

main函數(shù)中新開(kāi)個(gè)線(xiàn)程根據(jù)標(biāo)位run循環(huán),主線(xiàn)程中sleep一秒,然后設(shè)置run=false,大家認(rèn)為會(huì)打印"done ......."嗎?

答案就是不會(huì)打印,為什么呢?

JAVA并發(fā)三大特性

我們先來(lái)解釋下上面問(wèn)題的原因,如下圖所示,

現(xiàn)代的CPU架構(gòu)基本有多級(jí)緩存機(jī)制,t線(xiàn)程會(huì)將run加載到高速緩存中,然后主線(xiàn)程修改了主內(nèi)存的值為false,導(dǎo)致緩存不一致,但是t線(xiàn)程依然是從工作內(nèi)存中的高速緩存讀取run的值,最終無(wú)法跳出循環(huán)。

可見(jiàn)性

正如上面的例子,由于不做任何處理,一個(gè)線(xiàn)程能否立刻看到另外一個(gè)線(xiàn)程修改的共享變量值,我們稱(chēng)為"可見(jiàn)性"。

如果在并發(fā)程序中,不做任何處理,那么就會(huì)帶來(lái)可見(jiàn)性問(wèn)題,具體如何處理,見(jiàn)后文。

有序性

有序性是指程序按照代碼的先后順序執(zhí)行。但是編譯器或者處理器出于性能原因,改變程序語(yǔ)句的先后順序,比如代碼順序"a=1; b=2;",但是指令重排序后,有可能會(huì)變成"b=2;a=1", 那么這樣在并發(fā)情況下,會(huì)有問(wèn)題嗎?

在單線(xiàn)程情況下,指令重排序不會(huì)有任何影響。但是在并發(fā)情況下,可能會(huì)導(dǎo)致一些意想不到的bug。比如下面的例子:

public class Singleton {
  static Singleton instance;
    
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

假設(shè)有兩個(gè)線(xiàn)程 A、B 同時(shí)調(diào)用 getInstance() 方法,正常情況下,他們都可以拿到instance實(shí)例。

但往往bug就在一些極端的異常情況,比如new Singleton() 這個(gè)操作,實(shí)際會(huì)有下面3個(gè)步驟:

分配一塊內(nèi)存 M;

在內(nèi)存 M 上初始化 Singleton 對(duì)象;

然后 M 的地址賦值給 instance 變量。

現(xiàn)在發(fā)生指令重排序,順序變?yōu)橄旅娴姆绞剑?/p>

分配一塊內(nèi)存 M;

將 M 的地址賦值給 instance 變量;

最后在內(nèi)存 M 上初始化 Singleton 對(duì)象。

優(yōu)化后會(huì)導(dǎo)致什么問(wèn)題呢?我們假設(shè)線(xiàn)程 A 先執(zhí)行 getInstance() 方法,當(dāng)執(zhí)行完指令 2 時(shí)恰好發(fā)生了線(xiàn)程切換,切換到了線(xiàn)程 B 上;如果此時(shí)線(xiàn)程 B 也執(zhí)行 getInstance() 方法,那么線(xiàn)程 B 在執(zhí)行第一個(gè)判斷時(shí)會(huì)發(fā)現(xiàn) instance != null ,所以直接返回 instance,而此時(shí)的 instance 是沒(méi)有初始化過(guò)的,如果我們這個(gè)時(shí)候訪(fǎng)問(wèn) instance 的成員變量就可能觸發(fā)空指針異常。

這就是并發(fā)情況下,有序性帶來(lái)的一個(gè)問(wèn)題,這種情況又該如何處理呢?

當(dāng)然,指令重排序并不會(huì)瞎排序,處理器在進(jìn)行重排序時(shí),必須要考慮指令之間的數(shù)據(jù)依賴(lài)性。

原子性

如上圖所示,在多線(xiàn)程的情況下,CPU資源會(huì)在不同的線(xiàn)程間切換。那么這樣也會(huì)導(dǎo)致意向不到的問(wèn)題。

比如你認(rèn)為的一行代碼:count += 1,實(shí)際上涉及了多條CPU指令:

指令 1:首先,需要把變量 count 從內(nèi)存加載到 CPU 的寄存器; 指令 2:之后,在寄存器中執(zhí)行 +1 操作; 指令 3:最后,將結(jié)果寫(xiě)入內(nèi)存(緩存機(jī)制導(dǎo)致可能寫(xiě)入的是 CPU 緩存而不是內(nèi)存)。

操作系統(tǒng)做任務(wù)切換,可以發(fā)生在任何一條CPU 指令執(zhí)行完。假設(shè) count=0,如果線(xiàn)程 A 在指令 1 執(zhí)行完后做線(xiàn)程切換,線(xiàn)程 A 和線(xiàn)程 B 按照下圖的序列執(zhí)行,那么我們會(huì)發(fā)現(xiàn)兩個(gè)線(xiàn)程都執(zhí)行了 count+=1 的操作,但是得到的結(jié)果不是我們期望的 2,而是 1。

我們潛意識(shí)認(rèn)為的這個(gè)count+=1操作是一個(gè)不可分割的整體,就像一個(gè)原子一樣,我們把一個(gè)或者多個(gè)操作在 CPU 執(zhí)行的過(guò)程中不被中斷的特性稱(chēng)為原子性。但實(shí)際情況就是不做任何處理的話(huà),在并發(fā)情況下CPU進(jìn)行切換,導(dǎo)致出現(xiàn)原子性的問(wèn)題,我們一般通過(guò)加鎖解決,這個(gè)不是本文的重點(diǎn)。

Java內(nèi)存模型真面目

前面講解并發(fā)的三大特性,其中原子性問(wèn)題可以通過(guò)加鎖的方式解決,那么可見(jiàn)性和有序性有什么解決的方案呢?其實(shí)也很容易想到,可見(jiàn)性是因?yàn)榫彺鎸?dǎo)致,有序性是因?yàn)榫幾g優(yōu)化指令重排序?qū)е?,那么是不是可以讓程序員按需禁用緩存以及編譯優(yōu)化, 因?yàn)橹挥谐绦騿T知道什么情況下會(huì)出現(xiàn)問(wèn)題 。 順著這個(gè)思路,就提出了JAVA內(nèi)存模型(JMM)規(guī)范。

Java 內(nèi)存模型是 Java Memory Model(JMM),本身是一種抽象的概念,實(shí)際上并不存在,描述的是一組規(guī)則或規(guī)范,通過(guò)這組規(guī)范定義了程序中各個(gè)變量(包括實(shí)例字段,靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素)的訪(fǎng)問(wèn)方式。

默認(rèn)情況下,JMM中的內(nèi)存機(jī)制如下:

系統(tǒng)存在一個(gè)主內(nèi)存(Main Memory),Java 中所有變量都存儲(chǔ)在主存中,對(duì)于所有線(xiàn)程都是共享的 每條線(xiàn)程都有自己的工作內(nèi)存(Working Memory),工作內(nèi)存中保存的是主存中某些變量的拷貝 線(xiàn)程對(duì)所有變量的操作都是先對(duì)變量進(jìn)行拷貝,然后在工作內(nèi)存中進(jìn)行,不能直接操作主內(nèi)存中的變量 線(xiàn)程之間無(wú)法相互直接訪(fǎng)問(wèn),線(xiàn)程間的通信(傳遞)必須通過(guò)主內(nèi)存來(lái)完成

同時(shí),JMM規(guī)范了 JVM 如何提供按需禁用緩存和編譯優(yōu)化的方法,主要是通過(guò)volatile、synchronizedfinal 三個(gè)關(guān)鍵字,那具體的規(guī)則是什么樣的呢?

JMM 中的主內(nèi)存、工作內(nèi)存與 JVM 中的 Java 堆、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分,這兩者基本上是沒(méi)有關(guān)系的。

Happens-Before規(guī)則

JMM本質(zhì)上包含了一些規(guī)則,那這個(gè)規(guī)則就是大家有所耳聞的Happens-Before規(guī)則,大家都理解了些規(guī)則嗎?

Happens-Before規(guī)則,可以簡(jiǎn)單理解為如果想要A線(xiàn)程發(fā)生在B線(xiàn)程前面,也就是B線(xiàn)程能夠看到A線(xiàn)程,需要遵循6個(gè)原則。如果不符合 happens-before 規(guī)則,JMM 并不能保證一個(gè)線(xiàn)程的可見(jiàn)性和有序性。

1.程序的順序性規(guī)則

在一個(gè)線(xiàn)程中,邏輯上書(shū)寫(xiě)在前面的操作先行發(fā)生于書(shū)寫(xiě)在后面的操作。

這個(gè)規(guī)則很好理解,同一個(gè)線(xiàn)程中他們是用的同一個(gè)工作緩存,是可見(jiàn)的,并且多個(gè)操作之間有先后依賴(lài)關(guān)系,則不允許對(duì)這些操作進(jìn)行重排序。

2. volatile 變量規(guī)則

指對(duì)一個(gè) volatile 變量的寫(xiě)操作, Happens-Before 于后續(xù)對(duì)這個(gè) volatile 變量的讀操作。

怎么理解呢?比如線(xiàn)程A對(duì)volatile變量進(jìn)行寫(xiě)操作,那么線(xiàn)程B讀取這個(gè)volatile變量是可見(jiàn)的,就是說(shuō)能夠讀取到最新的值。

3.傳遞性

這條規(guī)則是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

這個(gè)規(guī)則也比較容易理解,不展開(kāi)討論了。

鎖的規(guī)則

這條規(guī)則是指對(duì)一個(gè)鎖的解鎖 Happens-Before于后續(xù)對(duì)這個(gè)鎖的加鎖,這里的鎖要是同一把鎖, 而且用synchronized或者ReentrantLock都可以。

如下代碼的例子:

synchronized (this) { // 此處自動(dòng)加鎖
  // x 是共享變量, 初始值 =10
  if (this.x < 12) {
    this.x = 12; 
  }  
} // 此處自動(dòng)解鎖

假設(shè) x 的初始值是 8,線(xiàn)程 A 執(zhí)行完代碼塊后 x 的值會(huì)變成 12(執(zhí)行完自動(dòng)釋放鎖) 線(xiàn)程 B 進(jìn)入代碼塊時(shí),能夠看到線(xiàn)程 A 對(duì) x 的寫(xiě)操作,也就是線(xiàn)程 B 能夠看到 x==12。

5.線(xiàn)程 start() 規(guī)則

主線(xiàn)程 A 啟動(dòng)子線(xiàn)程 B 后,子線(xiàn)程 B 能夠看到主線(xiàn)程在啟動(dòng)子線(xiàn)程 B 前的操作。

這個(gè)規(guī)則也很容易理解,線(xiàn)程 A 調(diào)用線(xiàn)程 B 的 start() 方法(即在線(xiàn)程 A 中啟動(dòng)線(xiàn)程 B),那么該 start() 操作 Happens-Before 于線(xiàn)程 B 中的任意操作。

6.線(xiàn)程 join() 規(guī)則

線(xiàn)程 A 中,調(diào)用線(xiàn)程 B 的 join() 并成功返回,那么線(xiàn)程 B 中的任意操作 Happens-Before 于該 join() 操作的返回。

使用JMM規(guī)則

我們現(xiàn)在已經(jīng)基本講清楚了JAVA內(nèi)存模型規(guī)范,以及里面關(guān)鍵的Happens-Before規(guī)則,那有啥用呢?回到前言的問(wèn)題中,我們是不是可以使用目前學(xué)到的關(guān)于JMM的知識(shí)去解決這個(gè)問(wèn)題。

方案一: 使用volatile

根據(jù)JMM的第2條規(guī)則,主線(xiàn)程寫(xiě)了volatile修飾的run變量,后面的t線(xiàn)程讀取的時(shí)候就可以看到了。

方案二:使用鎖

利用synchronized鎖的規(guī)則,主線(xiàn)程釋放鎖,那么后續(xù)t線(xiàn)程加鎖就可以看到之前的內(nèi)容了。

小結(jié):

volatile 關(guān)鍵字

保證可見(jiàn)性 不保證原子性 保證有序性(禁止指令重排)

volatile 修飾的變量進(jìn)行讀操作與普通變量幾乎沒(méi)什么差別,但是寫(xiě)操作相對(duì)慢一些,因?yàn)樾枰诒镜卮a中插入很多內(nèi)存屏障來(lái)保證指令不會(huì)發(fā)生亂序執(zhí)行,但是開(kāi)銷(xiāo)比鎖要小。volatile的性能遠(yuǎn)比加鎖要好。

synchronized 關(guān)鍵字

保證可見(jiàn)性 不保證原子性 保證有序性

加了鎖之后,只能有一個(gè)線(xiàn)程獲得到了鎖,獲得不到鎖的線(xiàn)程就要阻塞,所以同一時(shí)間只有一個(gè)線(xiàn)程執(zhí)行,相當(dāng)于單線(xiàn)程,由于數(shù)據(jù)依賴(lài)性的存在,單線(xiàn)程的指令重排是沒(méi)有問(wèn)題的。

線(xiàn)程加鎖前,將清空工作內(nèi)存中共享變量的值,使用共享變量時(shí)需要從主內(nèi)存中重新讀取最新的值;線(xiàn)程解鎖前,必須把共享變量的最新值刷新到主內(nèi)存中。

總結(jié)

本文講解了JAVA并發(fā)的3大特性,可見(jiàn)性、有序性和原子性。從而引出了JAVA內(nèi)存模型規(guī)范,這主要是為了解決并發(fā)情況下帶來(lái)的可見(jiàn)性和有序性問(wèn)題,主要就是定義了一些規(guī)則,需要我們程序員懂得這些規(guī)則,然后根據(jù)實(shí)際場(chǎng)景去使用,就是使用volatile、synchronizedfinal關(guān)鍵字,主要final關(guān)鍵字也會(huì)讓其他線(xiàn)程可見(jiàn),并且保證有序性。那么具體他們底層的實(shí)現(xiàn)是什么,是如何保證可見(jiàn)和有序的,我們后面詳細(xì)講解。

到此這篇關(guān)于JAVA內(nèi)存模型(JMM)詳解的文章就介紹到這了,更多相關(guān)JAVA內(nèi)存模型 JMM內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解Java8中的Lambda表達(dá)式

    詳解Java8中的Lambda表達(dá)式

    這篇文章主要介紹了Java8中的Lambda表達(dá)式的相關(guān)資料,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下
    2020-07-07
  • Spring?Framework六種常見(jiàn)設(shè)計(jì)模式

    Spring?Framework六種常見(jiàn)設(shè)計(jì)模式

    設(shè)計(jì)模式是軟件開(kāi)發(fā)的重要組成部分,本文借助spring來(lái)講解這個(gè)框架的設(shè)計(jì)模式,通過(guò)本文我們探討了spring如何利用這些模式來(lái)提供這些豐富的功能,對(duì)本文感興趣的朋友跟隨小編一起看看吧
    2023-06-06
  • java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例

    java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例

    這篇文章主要介紹了java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例,需要的朋友可以參考下
    2014-05-05
  • springboot 自定義配置Boolean屬性不生效的解決

    springboot 自定義配置Boolean屬性不生效的解決

    這篇文章主要介紹了springboot 自定義配置Boolean屬性不生效的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • 詳解如何在Java中實(shí)現(xiàn)懶加載

    詳解如何在Java中實(shí)現(xiàn)懶加載

    懶加載是一種常見(jiàn)的優(yōu)化技術(shù),它可以延遲對(duì)象的創(chuàng)建或初始化,直到對(duì)象第一次被使用時(shí)才進(jìn)行。在本文中,我們將介紹如何使用?Java?中的?Supplier?接口和雙重檢查鎖定模式來(lái)實(shí)現(xiàn)懶加載,并保證只初始化一次,希望對(duì)大家有所幫助
    2023-03-03
  • SpringBoot整合Lombok插件與使用詳解

    SpringBoot整合Lombok插件與使用詳解

    Lombok是Java開(kāi)發(fā)的插件,通過(guò)注解自動(dòng)生成常用代碼,如getter/setter,節(jié)省開(kāi)發(fā)時(shí)間,提高效率,它在編譯期生成方法,不影響性能,安裝Lombok需要添加Maven依賴(lài)和IDEA插件,使用注解如@Data、@Getter等簡(jiǎn)化代碼編寫(xiě),官網(wǎng)提供詳細(xì)文檔
    2024-09-09
  • java面試散列表及樹(shù)所對(duì)應(yīng)容器類(lèi)及HashMap沖突解決全面分析

    java面試散列表及樹(shù)所對(duì)應(yīng)容器類(lèi)及HashMap沖突解決全面分析

    這篇文章主要介紹了java面試中的java散列表及樹(shù)所對(duì)應(yīng)容器類(lèi)與HashMap沖突解決的問(wèn)題總結(jié),有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2021-10-10
  • Spring Boot Feign服務(wù)調(diào)用之間帶token問(wèn)題

    Spring Boot Feign服務(wù)調(diào)用之間帶token問(wèn)題

    這篇文章主要介紹了Spring Boot Feign服務(wù)調(diào)用之間帶token的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Spring?Cloud?Eureka高可用配置(踩坑記錄)

    Spring?Cloud?Eureka高可用配置(踩坑記錄)

    在進(jìn)行Eureka高可用配置時(shí),控制臺(tái)一直出現(xiàn)“......”的錯(cuò)誤,但是在瀏覽器中輸入地址:peer1:8761 卻是可正常運(yùn)行,這篇文章主要介紹了Spring?Cloud踩坑之Eureka高可用配置,需要的朋友可以參考下
    2023-08-08
  • 大話(huà)Java混合運(yùn)算規(guī)則

    大話(huà)Java混合運(yùn)算規(guī)則

    這篇文章主要介紹了大話(huà)Java混合運(yùn)算規(guī)則,小編覺(jué)得挺不錯(cuò)的,在這里分享給大家,需要的朋友可以了解下。
    2017-10-10

最新評(píng)論