
從動圖我們看到,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寄存器和高速緩存的抽象。
在文章上面,我對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)的流程

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ù)文章
發(fā)現(xiàn)“在看”和“贊”了嗎,因?yàn)槟愕狞c(diǎn)贊,讓我元?dú)鉂M滿哦