<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JUC并發(fā)編程之JMM內(nèi)存模型詳解

          共 6136字,需瀏覽 13分鐘

           ·

          2021-04-09 10:18

          點(diǎn)擊“藍(lán)字”關(guān)注我們吧


          JMM內(nèi)存模型概念



          JMM就是Java內(nèi)存模型(java memory model)本身是一種抽象的概念,并不真實(shí)存在,它描述的是一組規(guī)則或規(guī)范。因?yàn)樵诓煌挠布a(chǎn)商和不同的操作系統(tǒng)下,內(nèi)存的訪問有一定的差異,所以會造成相同的代碼運(yùn)行在不同的系統(tǒng)上會出現(xiàn)各種問題。所以java內(nèi)存模型(JMM)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實(shí)現(xiàn)讓java程序在各種平臺下都能達(dá)到一致的并發(fā)效果。
          Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,包括實(shí)例變量,靜態(tài)變量,但是不包括局部變量和方法參數(shù)。每個(gè)線程都有自己的工作內(nèi)存,線程工作內(nèi)存保存了該線程用到的變量和主內(nèi)存的副本拷貝,線程對變量的操作都在工作內(nèi)存中進(jìn)行。線程不能直接讀寫主內(nèi)存中的變量。
          不同的線程之間也無法訪問對方工作內(nèi)存中的變量。線程之間變量值的傳遞均需要通過主內(nèi)存來完成。


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



          jmm較為簡介的概念圖如下:


          主內(nèi)存
          主要存儲的是Java實(shí)例對象,所有線程創(chuàng)建的實(shí)例對象都存放在主內(nèi)存中,該實(shí)例對象是成員變量也包括了共享的類信息、常量、靜態(tài)變量。由于是共享數(shù)據(jù)區(qū)域,多條線程對同一個(gè)變量進(jìn)行訪問可能會發(fā)生線程安全問題。


          工作內(nèi)存

          主要存儲當(dāng)前方法的所有本地變量信息(工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝), 每個(gè)線程只能訪問自己的工作內(nèi)存,即線程中的本地變量對其它線程是不可見的,就算是兩個(gè)線程執(zhí)行的是同一段代碼,它們也會各自在自己的工作內(nèi)存中創(chuàng)建屬于當(dāng)前線程的本地變量,當(dāng)然也包括了字節(jié)碼行號指示器、相關(guān)Native方法的信息。注意由于工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù),線程間無法相互訪問工作內(nèi)存,因此存儲在工作內(nèi)存的數(shù)據(jù)不存在線程安全問題。(不要理解成棧幀,JMM內(nèi)存模型和JVM內(nèi)存模型是兩碼事)


          每個(gè)線程的工作內(nèi)存都是互相獨(dú)立的,線程操作的數(shù)據(jù)只能在工作內(nèi)存中操作,然后將操作完的值重新刷回主內(nèi)存中,這是java內(nèi)存模型定義的線程基本工作方式。

          需要注意的是:jmm的內(nèi)存模型與jvm的內(nèi)存模型概念不一樣的


          上述所說的內(nèi)容,可能還會有點(diǎn)抽象,結(jié)合一波代碼配合演示圖會更加好理解點(diǎn)

          @Slf4jpublic class Test01 {    private static boolean initFlag = false;    public static void refresh() {        log.info("refresh data.......");        initFlag = true;        log.info("refresh data success.......");    }    public static void main(String[] args) {        Thread threadA = new Thread(() -> {            while (!initFlag) {            }            log.info("線程:" + Thread.currentThread().getName()                    + "當(dāng)前線程嗅探到initFlag的狀態(tài)的改變");        }, "threadA");        threadA.start();        try {            Thread.sleep(500);        } catch (InterruptedException e) {            e.printStackTrace();        }        Thread threadB = new Thread(() -> {            refresh();        }, "threadB");        threadB.start();    }}


          運(yùn)行效果圖


          從動圖我們看到,A線程內(nèi)部判斷 "initFlag" 變量,如果變量false則一直進(jìn)行循環(huán),而代碼中的B線程內(nèi)部調(diào)用refresh()方法將變量 "initFlag" 修改為true,而此時(shí)A線程內(nèi)部的循環(huán)感應(yīng)到 "initFlag" 變量為true了應(yīng)該退出來才對,而為什么演示圖中A線程內(nèi)部的循環(huán)并沒有退出來?


          帶著這個(gè)疑惑,將代碼稍微改動一下,這次在代碼中定義一個(gè)全局變量為count為int類型,并在A線程循環(huán)中,將變量count自增操作,再來看看它的效果如何


          上圖,我們往循環(huán)內(nèi)部加一個(gè)count自增操作,貌似并沒有解決掉A線程循環(huán)的退出對嗎?不慌,這次我們往count變量上加一個(gè)volatile關(guān)鍵字,接著來看看效果


          欸,上圖中,我往count變量加了一個(gè)volatile關(guān)鍵字,A線程內(nèi)部的循環(huán)居然退出去,那么意味著A線程它檢測到了全局變量 "initFlag" 將值改變?yōu)閠rue了,那我們再接著測試一下,我們將count++操作去掉,往initFlag變量上加volatile關(guān)鍵字,在繼續(xù)看看效果如何?


          嗯,好家伙,似乎經(jīng)過這兩輪測試,其實(shí)可以大致猜出加了volatile關(guān)鍵字的原因,該篇文章不是講volatile的重點(diǎn),我來講講為什么發(fā)生這種情況。


          先再次看到這張圖


          這張圖,我前面已經(jīng)放上去了,現(xiàn)在再次粘過來,是為了更好的說明上述程序的問題所在,我上面有解釋過主內(nèi)存是專門存儲成員變量的,該成員變量的值是允許被多個(gè)線程進(jìn)行共享,那么上述程序中 "initFlag" 變量作為成員變量,是可以被A線程和B線程進(jìn)行讀取和操作的,那么此時(shí)A和B線程都會要進(jìn)行讀取共享變量,它們各自會從主內(nèi)存中將變量進(jìn)行拷貝到各自線程內(nèi)部的工作內(nèi)存中,接著B線程內(nèi)部調(diào)用了refresh()方法,將initFlag的值改為true,在jmm模型中,B線程它并不會直接將值改回到主內(nèi)存中,而是先將自己內(nèi)部的工作內(nèi)存的 "initFlag" 值改為true,然后再寫回主內(nèi)存中,雖說此時(shí)主內(nèi)存的值已經(jīng)發(fā)生了改變,但是A線程內(nèi)部的循環(huán)的判斷,還是在使用它自己內(nèi)部工作內(nèi)存中的 "initFlag" 的值,它并沒有及時(shí)的知道共享變量的值已經(jīng)發(fā)生了改變,所以這就導(dǎo)致了A線程長時(shí)間無法走出循環(huán)的原因。


          而我后面又在initFlag變量上加了volatile關(guān)鍵字,為什么能夠立馬感知到呢?

          說到這,我們需要了解到并發(fā)的三大特性內(nèi)容。


          JMM內(nèi)存模型定義

          JMM內(nèi)存模型主要通過三個(gè)特征組建成,1.原子性 2.可見性 3.有序性.這三個(gè)可謂是java并發(fā)的基礎(chǔ)


          原子性:

          原子性指的是一個(gè)操作是不可中斷的,即使是在多線程環(huán)境下,一個(gè)操作一旦開始就不會被其他線程影響。


          可見性:

          可見性指當(dāng)一個(gè)線程修改共享變量的值,其他線程能夠立即知道被修改了。Java是利用volatile關(guān)鍵字來提供可見性的。當(dāng)變量被volatile修飾時(shí),這個(gè)變量被修改后會立刻刷新到主內(nèi)存,當(dāng)其它線程需要讀取該變量時(shí),會去主內(nèi)存中讀取新值。而普通變量則不能保證這一點(diǎn)。(如果其他線程使用到了該變量,修改后會立刻刷新到主內(nèi)存,并且主動推送到其他線程的工作內(nèi)存中更新該變量值)
          看到此處,是不是就知道為什么加了volatile關(guān)鍵字,其他的線程能夠立馬感知到變量發(fā)生了變化。


          有序性:

          在并發(fā)情況下,能夠讓線程按代碼從上往下按順序進(jìn)行執(zhí)行,可以使用synchronized或者volatile保證多線程之間操作的有序性(這種是在沒有指令重排的情況下)



          JMM內(nèi)存模型與硬件架構(gòu)的關(guān)系



          通過對前面的硬件內(nèi)存架構(gòu)、Java內(nèi)存模型以及Java多線程的實(shí)現(xiàn)原理的了解,我們應(yīng)該已經(jīng)意識到,多線程的執(zhí)行最終都會映射到硬件處理器上進(jìn)行執(zhí)行,但Java內(nèi)存模型和硬件內(nèi)存架構(gòu)并不完全一致。對于硬件內(nèi)存來說只有寄存器、緩存內(nèi)存、主內(nèi)存的概念,并沒有工作內(nèi)存(線程私有數(shù)據(jù)區(qū)域)和主內(nèi)存(堆內(nèi)存)之分,也就是說Java內(nèi)存模型對內(nèi)存的劃分對硬件內(nèi)存并沒有任何影響,因?yàn)镴MM只是一種抽象的概念,是一組規(guī)則,并不實(shí)際存在,不管是工作內(nèi)存的數(shù)據(jù)還是主內(nèi)存的數(shù)據(jù),對于計(jì)算機(jī)硬件來說都會存儲在計(jì)算機(jī)主內(nèi)存中,當(dāng)然也有可能存儲到CPU緩存或者寄存器中,因此總體上來說,Java內(nèi)存模型和計(jì)算機(jī)硬件內(nèi)存架構(gòu)是一個(gè)相互交叉的關(guān)系,是一種抽象概念劃分與真實(shí)物理硬件的交叉。


          結(jié)論:在JVM的內(nèi)存模型中,每個(gè)線程有自己的工作內(nèi)存,實(shí)際上JAVA線程借助了底層操作系統(tǒng)線程實(shí)現(xiàn),一個(gè)JVM線程對應(yīng)一個(gè)操作系統(tǒng)線程,線程的工作內(nèi)存其實(shí)是cpu寄存器和高速緩存的抽象。



          數(shù)據(jù)同步八大原子操作



          在文章上面,我對JMM模型的代碼案例以及圖都做了一個(gè)比較清楚的解釋,但是主內(nèi)存中的共享變量的值是如何copy到線程內(nèi)部的工作內(nèi)存中的呢?這里就涉及到數(shù)據(jù)同步八大原子操作,且看下圖。


          (1)lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài)
          (2)unlock(解鎖):作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定
          (3)read(讀取):作用于主內(nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用  
          (4)load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中
          (5)use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎  
          (6)assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
          (7)store(存儲):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作
          (8)write(寫入):作用于工作內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存的變量中


          如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存中,就需要按順序地執(zhí)行read和load操作,如果把變量從工作內(nèi)存中同步到主內(nèi)存中,就需要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行。


          結(jié)合上面的代碼例子,通過八大原子操作,實(shí)現(xiàn)的流程


          同步規(guī)則分析
          1)不允許一個(gè)線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中
          2)一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或者assign)的變量。即就是對一個(gè)變量實(shí)施use和store操作之前,必須先自行assign和load操作。
          3)一個(gè)變量在同一時(shí)刻只允許一條線程對其進(jìn)行l(wèi)ock操作,但lock操作可以被同一線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖。lock和unlock必須成對出現(xiàn)。
          4)如果對一個(gè)變量執(zhí)行l(wèi)ock操作,將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量之前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值。
          5)如果一個(gè)變量事先沒有被lock操作鎖定,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
          6)對一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)


          最后看到這,我前面似乎還漏了一個(gè)問題沒有講到,我稍微回顧一下案例場景,上面代碼案例中,我定義了一個(gè) initFlag變量,通過B線程將變量的值進(jìn)行改變,發(fā)現(xiàn)A線程循環(huán)內(nèi)部無法跳出循環(huán)的問題,后來又額外的定義了一個(gè)count變量,用于在A線程循環(huán)內(nèi)部做自增的操作,然后讓它進(jìn)行跳出來,發(fā)現(xiàn)該方法并不可行,然后又繼續(xù)往count值上加了一個(gè)volatile關(guān)鍵字,它就能夠立馬被A線程感知到,看到這可能還感受不到問題的存在,那么再仔細(xì)想想,結(jié)合前面的JMM內(nèi)存模型的圖,我在initFlag變量上加了volatile關(guān)鍵字,它能夠被立馬感知到,這是非常符合邏輯的,但是問題出現(xiàn)在于為什么我將關(guān)鍵字加在了count變量上,initFlag變量也能夠被感知到呢?

          這里我想回答的是,在cpu底層的緩存行中,它的每個(gè)緩存行大小為64個(gè)字節(jié),而我們的initFlag變量它只占用了一個(gè)字節(jié),且count變量它占用了4個(gè)字節(jié),它們在緩存行中總共5個(gè)字節(jié),當(dāng)緩存行中的某一個(gè)變量的值發(fā)生了修改,volatile關(guān)鍵字會強(qiáng)行通知線程去拉取最新變量的值。所以這就是為什么我在count變量上加了關(guān)鍵字,其他線程能夠及時(shí)的感知到initFlag的值發(fā)生了改變的原因。


          最后我還想說明的一點(diǎn)是,無論我們是否加了volatile關(guān)鍵字,線程遲早會知道變量發(fā)生了改變,只不過區(qū)別在于關(guān)鍵字能夠及時(shí)的通知線程變量發(fā)生了改變。


          我是黎明大大,我知道我沒有驚世的才華,也沒有超于凡人的能力,但畢竟我還有一個(gè)不屈服,敢于選擇向命運(yùn)沖鋒的靈魂,和一個(gè)就是傷痕累累也要義無反顧走下去的心。


          如果您覺得本文對您有幫助,還請關(guān)注點(diǎn)贊一波,后期將不間斷更新更多技術(shù)文章


          掃描二維碼關(guān)注我
          不定期更新技術(shù)文章哦



          深入Hotspot源碼與Linux內(nèi)核理解NIO與Epoll

          RabbitMQ高級特性之延遲隊(duì)列

          RabbitMQ高級特性之消費(fèi)端限流

          RabbitMQ六種隊(duì)列模式之主題模式



          發(fā)現(xiàn)“在看”和“贊”了嗎,因?yàn)槟愕狞c(diǎn)贊,讓我元?dú)鉂M滿哦
          瀏覽 26
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久久久久无码麻豆 | 一区二区不卡无码免费 | 中国黄色操逼大片 | 啪啪网站视频 | 国产精品高潮视频 |