Java內(nèi)存模型(JMM)
我們常說(shuō)的JVM內(nèi)存模式指的是JVM的內(nèi)存分區(qū);而Java內(nèi)存模式是一種虛擬機(jī)規(guī)范。

Java虛擬機(jī)規(guī)范中定義了Java內(nèi)存模型(Java Memory Model,JMM),用于屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的并發(fā)效果,JMM規(guī)范了Java虛擬機(jī)與計(jì)算機(jī)內(nèi)存是如何協(xié)同工作的:規(guī)定了一個(gè)線程如何和何時(shí)可以看到由其他線程修改過(guò)后的共享變量的值,以及在必須時(shí)如何同步的訪問(wèn)共享變量。
原始的Java內(nèi)存模型存在一些不足,因此Java內(nèi)存模型在Java1.5時(shí)被重新修訂。這個(gè)版本的Java內(nèi)存模型在Java8中仍然在使用。
Java內(nèi)存模型(不僅僅是JVM內(nèi)存分區(qū)):調(diào)用棧和本地變量存放在線程棧上,對(duì)象存放在堆上。


- 一個(gè)本地變量可能是原始類型,在這種情況下,它總是“呆在”線程棧上。
- 一個(gè)本地變量也可能是指向一個(gè)對(duì)象的一個(gè)引用。在這種情況下,引用(這個(gè)本地變量)存放在線程棧上,但是對(duì)象本身存放在堆上。
- 一個(gè)對(duì)象可能包含方法,這些方法可能包含本地變量。這些本地變量仍然存放在線程棧上,即使這些方法所屬的對(duì)象存放在堆上。
- 一個(gè)對(duì)象的成員變量可能隨著這個(gè)對(duì)象自身存放在堆上。不管這個(gè)成員變量是原始類型還是引用類型。
- 靜態(tài)成員變量跟隨著類定義一起也存放在堆上。
- 存放在堆上的對(duì)象可以被所有持有對(duì)這個(gè)對(duì)象引用的線程訪問(wèn)。當(dāng)一個(gè)線程可以訪問(wèn)一個(gè)對(duì)象時(shí),它也可以訪問(wèn)這個(gè)對(duì)象的成員變量。如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)對(duì)象上的同一個(gè)方法,它們將會(huì)都訪問(wèn)這個(gè)對(duì)象的成員變量,但是每一個(gè)線程都擁有這個(gè)成員變量的私有拷貝。
硬件內(nèi)存架構(gòu)
現(xiàn)代硬件內(nèi)存模型與Java內(nèi)存模型有一些不同,理解內(nèi)存模型架構(gòu)以及Java內(nèi)存模型如何與它協(xié)同工作也是非常重要的。
現(xiàn)代計(jì)算機(jī)硬件架構(gòu)的簡(jiǎn)單圖示:

- 多CPU:一個(gè)現(xiàn)代計(jì)算機(jī)通常由兩個(gè)或者多個(gè)CPU。其中一些CPU還有多核。從這一點(diǎn)可以看出,在一個(gè)有兩個(gè)或者多個(gè)CPU的現(xiàn)代計(jì)算機(jī)上同時(shí)運(yùn)行多個(gè)線程是可能的。每個(gè)CPU在某一時(shí)刻運(yùn)行一個(gè)線程是沒(méi)有問(wèn)題的。這意味著,如果你的Java程序是多線程的,在你的Java程序中每個(gè)CPU上一個(gè)線程可能同時(shí)(并發(fā))執(zhí)行。
- CPU寄存器:每個(gè)CPU都包含一系列的寄存器,它們是CPU內(nèi)內(nèi)存的基礎(chǔ)。CPU在寄存器上執(zhí)行操作的速度遠(yuǎn)大于在主存上執(zhí)行的速度。這是因?yàn)镃PU訪問(wèn)寄存器的速度遠(yuǎn)大于主存。
- 高速緩存cache:由于計(jì)算機(jī)的存儲(chǔ)設(shè)備與處理器的運(yùn)算速度之間有著幾個(gè)數(shù)量級(jí)的差距,所以現(xiàn)代計(jì)算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運(yùn)算速度的高速緩存(Cache)來(lái)作為內(nèi)存與處理器之間的緩沖:將運(yùn)算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運(yùn)算能快速進(jìn)行,當(dāng)運(yùn)算結(jié)束后再?gòu)木彺嫱交貎?nèi)存之中,這樣處理器就無(wú)須等待緩慢的內(nèi)存讀寫了。CPU訪問(wèn)緩存層的速度快于訪問(wèn)主存的速度,但通常比訪問(wèn)內(nèi)部寄存器的速度還要慢一點(diǎn)。每個(gè)CPU可能有一個(gè)CPU緩存層,一些CPU還有多層緩存。在某一時(shí)刻,一個(gè)或者多個(gè)緩存行(cache lines)可能被讀到緩存,一個(gè)或者多個(gè)緩存行可能再被刷新回主存。
- 內(nèi)存:一個(gè)計(jì)算機(jī)還包含一個(gè)主存。所有的CPU都可以訪問(wèn)主存。主存通常比CPU中的緩存大得多。
- 運(yùn)作原理:通常情況下,當(dāng)一個(gè)CPU需要讀取主存時(shí),它會(huì)將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內(nèi)容讀到它的內(nèi)部寄存器中,然后在寄存器中執(zhí)行操作。當(dāng)CPU需要將結(jié)果寫回到主存中去時(shí),它會(huì)將內(nèi)部寄存器的值刷新到緩存中,然后在某個(gè)時(shí)間點(diǎn)將值刷新回主存。
一些問(wèn)題:(多線程環(huán)境下尤其)
- 緩存一致性問(wèn)題:在多處理器系統(tǒng)中,每個(gè)處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(MainMemory)?;诟咚倬彺娴拇鎯?chǔ)交互很好地解決了處理器與內(nèi)存的速度矛盾,但是也引入了新的問(wèn)題:緩存一致性(CacheCoherence)。當(dāng)多個(gè)處理器的運(yùn)算任務(wù)都涉及同一塊主內(nèi)存區(qū)域時(shí),將可能導(dǎo)致各自的緩存數(shù)據(jù)不一致的情況,如果真的發(fā)生這種情況,那同步回到主內(nèi)存時(shí)以誰(shuí)的緩存數(shù)據(jù)為準(zhǔn)呢?為了解決一致性的問(wèn)題,需要各個(gè)處理器訪問(wèn)緩存時(shí)都遵循一些協(xié)議,在讀寫時(shí)要根據(jù)協(xié)議來(lái)進(jìn)行操作,這類協(xié)議有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等:

- 指令重排序問(wèn)題:為了使得處理器內(nèi)部的運(yùn)算單元能盡量被充分利用,處理器可能會(huì)對(duì)輸入代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會(huì)在計(jì)算之后將亂序執(zhí)行的結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的,但并不保證程序中各個(gè)語(yǔ)句計(jì)算的先后順序與輸入代碼中的順序一致。因此,如果存在一個(gè)計(jì)算任務(wù)依賴另一個(gè)計(jì)算任務(wù)的中間結(jié)果,那么其順序性并不能靠代碼的先后順序來(lái)保證。與處理器的亂序執(zhí)行優(yōu)化類似,Java虛擬機(jī)的即時(shí)編譯器中也有類似的指令重排序(Instruction Reorder)優(yōu)化
Java內(nèi)存模型和硬件內(nèi)存架構(gòu)之間的橋接
Java內(nèi)存模型與硬件內(nèi)存架構(gòu)之間存在差異。硬件內(nèi)存架構(gòu)沒(méi)有區(qū)分線程棧和堆。對(duì)于硬件,所有的線程棧和堆都分布在主內(nèi)存中。部分線程棧和堆可能有時(shí)候會(huì)出現(xiàn)在CPU緩存中和CPU內(nèi)部的寄存器中。如下圖所示:

從抽象的角度來(lái)看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:
- 線程之間的共享變量存儲(chǔ)在主內(nèi)存(Main Memory)中
- 每個(gè)線程都有一個(gè)私有的本地內(nèi)存(Local Memory),本地內(nèi)存是JMM的一個(gè)抽象概念,并不真實(shí)存在,它涵蓋了緩存、寫緩沖區(qū)、寄存器以及其他的硬件和編譯器優(yōu)化。本地內(nèi)存中存儲(chǔ)了該線程以讀/寫共享變量的拷貝副本。
- 從更低的層次來(lái)說(shuō),主內(nèi)存就是硬件的內(nèi)存,而為了獲取更好的運(yùn)行速度,虛擬機(jī)及硬件系統(tǒng)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中。
- Java內(nèi)存模型中的線程的工作內(nèi)存(working memory)是cpu的寄存器和高速緩存的抽象描述。而JVM的靜態(tài)內(nèi)存儲(chǔ)模型(JVM內(nèi)存模型)只是一種對(duì)內(nèi)存的物理劃分而已,它只局限在內(nèi)存,而且只局限在JVM的內(nèi)存。


JMM模型下的線程間通信:
線程間通信必須要經(jīng)過(guò)主內(nèi)存。
如下,如果線程A與線程B之間要通信的話,必須要經(jīng)歷下面2個(gè)步驟:
1)線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去。
2)線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量。

關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議,即一個(gè)變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實(shí)現(xiàn)細(xì)節(jié),Java內(nèi)存模型定義了以下八種操作來(lái)完成:
- lock(鎖定):作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占狀態(tài)。
- unlock(解鎖):作用于主內(nèi)存變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定。
- read(讀?。?/strong>:作用于主內(nèi)存變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便隨后的load動(dòng)作使用
- load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
- use(使用):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作。
- assign(賦值):作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作。
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中,以便隨后的write的操作。
- write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個(gè)變量的值傳送到主內(nèi)存的變量中。

Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時(shí),必須滿足如下規(guī)則:
- 如果要把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存,就需要按順尋地執(zhí)行read和load操作, 如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作。但Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒(méi)有保證必須是連續(xù)執(zhí)行。
- 不允許read和load、store和write操作之一單獨(dú)出現(xiàn)
- 不允許一個(gè)線程丟棄它的最近assign的操作,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中。
- 不允許一個(gè)線程無(wú)原因地(沒(méi)有發(fā)生過(guò)任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中。
- 一個(gè)新的變量只能在主內(nèi)存中誕生,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化(load或assign)的變量。即就是對(duì)一個(gè)變量實(shí)施use和store操作之前,必須先執(zhí)行過(guò)了assign和load操作。
- 一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作,但lock操作可以被同一條線程重復(fù)執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會(huì)被解鎖。lock和unlock必須成對(duì)出現(xiàn)
- 如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
- 如果一個(gè)變量事先沒(méi)有被lock操作鎖定,則不允許對(duì)它執(zhí)行unlock操作;也不允許去unlock一個(gè)被其他線程鎖定的變量。
- 對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。
Java內(nèi)存模型解決的問(wèn)題
當(dāng)對(duì)象和變量被存放在計(jì)算機(jī)中各種不同的內(nèi)存區(qū)域中時(shí),就可能會(huì)出現(xiàn)一些具體的問(wèn)題。Java內(nèi)存模型建立所圍繞的問(wèn)題:在多線程并發(fā)過(guò)程中,如何處理多線程讀同步問(wèn)題與可見(jiàn)性(多線程緩存與指令重排序)、多線程寫同步問(wèn)題與原子性(多線程競(jìng)爭(zhēng)race condition)。
1、多線程讀同步與可見(jiàn)性
可見(jiàn)性(共享對(duì)象可見(jiàn)性):線程對(duì)共享變量修改的可見(jiàn)性。當(dāng)一個(gè)線程修改了共享變量的值,其他線程能夠立刻得知這個(gè)修改
線程緩存導(dǎo)致的可見(jiàn)性問(wèn)題:
如果兩個(gè)或者更多的線程在沒(méi)有正確的使用volatile聲明或者同步的情況下共享一個(gè)對(duì)象,一個(gè)線程更新這個(gè)共享對(duì)象可能對(duì)其它線程來(lái)說(shuō)是不可見(jiàn)的:共享對(duì)象被初始化在主存中。跑在CPU上的一個(gè)線程將這個(gè)共享對(duì)象讀到CPU緩存中,然后修改了這個(gè)對(duì)象。只要CPU緩存沒(méi)有被刷新會(huì)主存,對(duì)象修改后的版本對(duì)跑在其它CPU上的線程都是不可見(jiàn)的。這種方式可能導(dǎo)致每個(gè)線程擁有這個(gè)共享對(duì)象的私有拷貝,每個(gè)拷貝停留在不同的CPU緩存中。
下圖示意了這種情形。跑在左邊CPU的線程拷貝這個(gè)共享對(duì)象到它的CPU緩存中,然后將count變量的值修改為2。這個(gè)修改對(duì)跑在右邊CPU上的其它線程是不可見(jiàn)的,因?yàn)樾薷暮蟮腸ount的值還沒(méi)有被刷新回主存中去。

解決這個(gè)內(nèi)存可見(jiàn)性問(wèn)題你可以使用:
- Java中的volatile關(guān)鍵字:volatile關(guān)鍵字可以保證直接從主存中讀取一個(gè)變量,如果這個(gè)變量被修改后,總是會(huì)被寫回到主存中去。Java內(nèi)存模型是通過(guò)在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來(lái)實(shí)現(xiàn)可見(jiàn)性的,無(wú)論是普通變量還是volatile變量都是如此,普通變量與volatile變量的區(qū)別是:volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每個(gè)線程在每次使用volatile變量前都立即從主內(nèi)存刷新。因此我們可以說(shuō)volatile保證了多線程操作時(shí)變量的可見(jiàn)性,而普通變量則不能保證這一點(diǎn)。
- Java中的synchronized關(guān)鍵字:同步快的可見(jiàn)性是由“如果對(duì)一個(gè)變量執(zhí)行l(wèi)ock操作,將會(huì)清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個(gè)變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值”、“對(duì)一個(gè)變量執(zhí)行unlock操作之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store和write操作)”這兩條規(guī)則獲得的。
- Java中的final關(guān)鍵字:final關(guān)鍵字的可見(jiàn)性是指,被final修飾的字段在構(gòu)造器中一旦被初始化完成,并且構(gòu)造器沒(méi)有把“this”的引用傳遞出去(this引用逃逸是一件很危險(xiǎn)的事情,其他線程有可能通過(guò)這個(gè)引用訪問(wèn)到“初始化了一半”的對(duì)象),那么在其他線程就能看見(jiàn)final字段的值(無(wú)須同步)
重排序?qū)е碌目梢?jiàn)性問(wèn)題:
Java程序中天然的有序性可以總結(jié)為一句話:如果在本地線程內(nèi)觀察,所有操作都是有序的(“線程內(nèi)表現(xiàn)為串行”(Within-Thread As-If-Serial Semantics));如果在一個(gè)線程中觀察另一個(gè)線程,所有操作都是無(wú)序的(“指令重排序”現(xiàn)象和“線程工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象)。
Java語(yǔ)言提供了volatile和synchronized兩個(gè)關(guān)鍵字來(lái)保證線程之間操作的有序性:
- volatile關(guān)鍵字本身就包含了禁止指令重排序的語(yǔ)義
- synchronized則是由“一個(gè)變量在同一個(gè)時(shí)刻只允許一條線程對(duì)其進(jìn)行l(wèi)ock操作”這條規(guī)則獲得的,這個(gè)規(guī)則決定了持有同一個(gè)鎖的兩個(gè)同步塊只能串行地進(jìn)入
指令序列的重排序:
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語(yǔ)義的前提下,可以重新安排語(yǔ)句的執(zhí)行順序。
2)指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-LevelParallelism,ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。
3)內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行。

每個(gè)處理器上的寫緩沖區(qū),僅僅對(duì)它所在的處理器可見(jiàn)。這會(huì)導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會(huì)與內(nèi)存實(shí)際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會(huì)使用寫緩沖區(qū),因此現(xiàn)代的處理器都會(huì)允許對(duì)寫-讀操作進(jìn)行重排序:

數(shù)據(jù)依賴:
編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。(這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮)

指令重排序?qū)?nèi)存可見(jiàn)性的影響:

當(dāng)1和2之間沒(méi)有數(shù)據(jù)依賴關(guān)系時(shí),1和2之間就可能被重排序(3和4類似)。這樣的結(jié)果就是:讀線程B執(zhí)行4時(shí),不一定能看到寫線程A在執(zhí)行1時(shí)對(duì)共享變量的修改。
指令重排序改變多線程程序的執(zhí)行結(jié)果例子:

flag變量是個(gè)標(biāo)記,用來(lái)標(biāo)識(shí)變量a是否已被寫入。這里假設(shè)有兩個(gè)線程A和B,A首先執(zhí)行writer()方法,隨后B線程接著執(zhí)行reader()方法。線程B在執(zhí)行操作4時(shí),能否看到線程A在操作1對(duì)共享變量a的寫入呢?
答案是:不一定能看到。
由于操作1和操作2沒(méi)有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以對(duì)這兩個(gè)操作重排序;同樣,操作3和操作4沒(méi)有數(shù)據(jù)依賴關(guān)系,編譯器和處理器也可以對(duì)這兩個(gè)操作重排序。
as-if-serial語(yǔ)義:
不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。(編譯器、runtime和處理器都必須遵守as-if-serial語(yǔ)義)
happens before:
從JDK 5開始,Java使用新的JSR-133內(nèi)存模型,JSR-133使用happens-before的概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性:在JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn)(兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間),那么這兩個(gè)操作之間必須要存在happens-before關(guān)系:
- 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。
- volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
一個(gè)happens-before規(guī)則對(duì)應(yīng)于一個(gè)或多個(gè)編譯器和處理器重排序規(guī)則
內(nèi)存屏障禁止特定類型的處理器重排序:
重排序可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí),插入特定類型的內(nèi)存屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。通過(guò)禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
為了保證內(nèi)存可見(jiàn)性,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序。

StoreLoad Barriers是一個(gè)“全能型”的屏障,它同時(shí)具有其他3個(gè)屏障的效果?,F(xiàn)代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執(zhí)行該屏障開銷會(huì)很昂貴,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)。
2、多線程寫同步與原子性
多線程競(jìng)爭(zhēng)(Race Conditions)問(wèn)題:當(dāng)讀,寫和檢查共享變量時(shí)出現(xiàn)race conditions。
如果兩個(gè)或者更多的線程共享一個(gè)對(duì)象,多個(gè)線程在這個(gè)共享對(duì)象上更新變量,就有可能發(fā)生race conditions。
想象一下,如果線程A讀一個(gè)共享對(duì)象的變量count到它的CPU緩存中。再想象一下,線程B也做了同樣的事情,但是往一個(gè)不同的CPU緩存中?,F(xiàn)在線程A將count加1,線程B也做了同樣的事情?,F(xiàn)在count已經(jīng)被增加了兩次,每個(gè)CPU緩存中一次。如果這些增加操作被順序的執(zhí)行,變量count應(yīng)該被增加兩次,然后原值+2被寫回到主存中去。然而,兩次增加都是在沒(méi)有適當(dāng)?shù)耐较虏l(fā)執(zhí)行的。無(wú)論是線程A還是線程B將count修改后的版本寫回到主存中取,修改后的值僅會(huì)被原值大1,盡管增加了兩次:

解決這個(gè)問(wèn)題可以使用Java同步塊。一個(gè)同步塊可以保證在同一時(shí)刻僅有一個(gè)線程可以進(jìn)入代碼的臨界區(qū)。同步塊還可以保證代碼塊中所有被訪問(wèn)的變量將會(huì)從主存中讀入,當(dāng)線程退出同步代碼塊時(shí),所有被更新的變量都會(huì)被刷新回主存中去,不管這個(gè)變量是否被聲明為volatile。
使用原子性保證多線程寫同步問(wèn)題:
原子性:指一個(gè)操作是按原子的方式執(zhí)行的。要么該操作不被執(zhí)行;要么以原子方式執(zhí)行,即執(zhí)行過(guò)程中不會(huì)被其它線程中斷。
- Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
- Reads and writes are atomic for all variables declared volatile (including long and double variables).
(https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html)
實(shí)現(xiàn)原子性:
- 由Java內(nèi)存模型來(lái)直接保證的原子性變量操作包括read、load、assign、use、store、write,我們大致可以認(rèn)為基本數(shù)據(jù)類型變量、引用類型變量、聲明為volatile的任何類型變量的訪問(wèn)讀寫是具備原子性的(long和double的非原子性協(xié)定:對(duì)于64位的數(shù)據(jù),如long和double,Java內(nèi)存模型規(guī)范允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來(lái)進(jìn)行,即允許虛擬機(jī)實(shí)現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、store、read和write這四個(gè)操作的原子性,即如果有多個(gè)線程共享一個(gè)并未聲明為volatile的long或double類型的變量,并且同時(shí)對(duì)它們進(jìn)行讀取和修改操作,那么某些線程可能會(huì)讀取到一個(gè)既非原值,也不是其他線程修改值的代表了“半個(gè)變量”的數(shù)值。但由于目前各種平臺(tái)下的商用虛擬機(jī)幾乎都選擇把64位數(shù)據(jù)的讀寫操作作為原子操作來(lái)對(duì)待,因此在編寫代碼時(shí)一般也不需要將用到的long和double變量專門聲明為volatile)。這些類型變量的讀、寫天然具有原子性,但類似于 “基本變量++” / “volatile++” 這種復(fù)合操作并沒(méi)有原子性。
- 如果應(yīng)用場(chǎng)景需要一個(gè)更大范圍的原子性保證,需要使用同步塊技術(shù)。Java內(nèi)存模型提供了lock和unlock操作來(lái)滿足這種需求。虛擬機(jī)提供了字節(jié)碼指令monitorenter和monitorexist來(lái)隱式地使用這兩個(gè)操作,這兩個(gè)字節(jié)碼指令反映到Java代碼中就是同步快——synchronized關(guān)鍵字。
JMM對(duì)特殊Java語(yǔ)義的特殊規(guī)則支持
volatile總結(jié) (保證內(nèi)存可見(jiàn)性:Lock前綴的指令、內(nèi)存屏障禁止重排序)
synchronized總結(jié) (保證內(nèi)存可見(jiàn)性和操作原子性:互斥鎖;鎖優(yōu)化)
1. 三大性質(zhì)簡(jiǎn)介
在并發(fā)編程中分析線程安全的問(wèn)題時(shí)往往需要切入點(diǎn),那就是兩大核心:JMM抽象內(nèi)存模型以及happens-before規(guī)則(在這篇文章中已經(jīng)經(jīng)過(guò)了),三條性質(zhì):原子性,有序性和可見(jiàn)性。關(guān)于synchronized和volatile已經(jīng)討論過(guò)了,就想著將并發(fā)編程中這兩大神器在 原子性,有序性和可見(jiàn)性上做一個(gè)比較,當(dāng)然這也是面試中的高頻考點(diǎn),值得注意。
2. 原子性
原子性是指一個(gè)操作是不可中斷的,要么全部執(zhí)行成功要么全部執(zhí)行失敗,有著“同生共死”的感覺(jué)。及時(shí)在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開始,就不會(huì)被其他線程所干擾。我們先來(lái)看看哪些是原子操作,哪些不是原子操作,有一個(gè)直觀的印象:
int a = 10; //1
a++; //2
int b=a; //3
a = a+1; //4
上面這四個(gè)語(yǔ)句中只有第1個(gè)語(yǔ)句是原子操作,將10賦值給線程工作內(nèi)存的變量a,而語(yǔ)句2(a++),實(shí)際上包含了三個(gè)操作:1. 讀取變量a的值;2:對(duì)a進(jìn)行加一的操作;3.將計(jì)算后的值再賦值給變量a,而這三個(gè)操作無(wú)法構(gòu)成原子操作。對(duì)語(yǔ)句3,4的分析同理可得這兩條語(yǔ)句不具備原子性。當(dāng)然,java內(nèi)存模型中定義了8中操作都是原子的,不可再分的。
- lock(鎖定):作用于主內(nèi)存中的變量,它把一個(gè)變量標(biāo)識(shí)為一個(gè)線程獨(dú)占的狀態(tài);
- unlock(解鎖):作用于主內(nèi)存中的變量,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后的變量才可以被其他線程鎖定
- read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中,以便后面的load動(dòng)作使用;
- load(載入):作用于工作內(nèi)存中的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存中的變量副本
- use(使用):作用于工作內(nèi)存中的變量,它把工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用到變量的值的字節(jié)碼指令時(shí)將會(huì)執(zhí)行這個(gè)操作;
- assign(賦值):作用于工作內(nèi)存中的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)執(zhí)行這個(gè)操作;
- store(存儲(chǔ)):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個(gè)變量的值傳送給主內(nèi)存中以便隨后的write操作使用;
- write(操作):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中。
上面的這些指令操作是相當(dāng)?shù)讓拥?,可以作為擴(kuò)展知識(shí)面掌握下。那么如何理解這些指令了?比如,把一個(gè)變量從主內(nèi)存中復(fù)制到工作內(nèi)存中就需要執(zhí)行read,load操作,將工作內(nèi)存同步到主內(nèi)存中就需要執(zhí)行store,write操作。注意的是:java內(nèi)存模型只是要求上述兩個(gè)操作是順序執(zhí)行的并不是連續(xù)執(zhí)行的。也就是說(shuō)read和load之間可以插入其他指令,store和writer可以插入其他指令。比如對(duì)主內(nèi)存中的a,b進(jìn)行訪問(wèn)就可以出現(xiàn)這樣的操作順序:read a,read b, load b,load a。
由原子性變量操作read,load,use,assign,store,write,可以大致認(rèn)為基本數(shù)據(jù)類型的訪問(wèn)讀寫具備原子性(例外就是long和double的非原子性協(xié)定)
synchronized
上面一共有八條原子操作,其中六條可以滿足基本數(shù)據(jù)類型的訪問(wèn)讀寫具備原子性,還剩下lock和unlock兩條原子操作。如果我們需要更大范圍的原子性操作就可以使用lock和unlock原子操作。盡管jvm沒(méi)有把lock和unlock開放給我們使用,但jvm以更高層次的指令monitorenter和monitorexit指令開放給我們使用,反應(yīng)到j(luò)ava代碼中就是---synchronized關(guān)鍵字,也就是說(shuō)synchronized滿足原子性。
volatile
我們先來(lái)看這樣一個(gè)例子:
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
開啟10個(gè)線程,每個(gè)線程都自加10000次,如果不出現(xiàn)線程安全的問(wèn)題最終的結(jié)果應(yīng)該就是:10*10000 = 100000;可是運(yùn)行多次都是小于100000的結(jié)果,問(wèn)題在于 volatile并不能保證原子性,在前面說(shuō)過(guò)counter++這并不是一個(gè)原子操作,包含了三個(gè)步驟:1.讀取變量counter的值;2.對(duì)counter加一;3.將新值賦值給變量counter。如果線程A讀取counter到工作內(nèi)存后,其他線程對(duì)這個(gè)值已經(jīng)做了自增操作后,那么線程A的這個(gè)值自然而然就是一個(gè)過(guò)期的值,因此,總結(jié)果必然會(huì)是小于100000的。
如果讓volatile保證原子性,必須符合以下兩條規(guī)則:
- 運(yùn)算結(jié)果并不依賴于變量的當(dāng)前值,或者能夠確保只有一個(gè)線程修改變量的值;
- 變量不需要與其他的狀態(tài)變量共同參與不變約束
3. 有序性
synchronized
synchronized語(yǔ)義表示鎖在同一時(shí)刻只能由一個(gè)線程進(jìn)行獲取,當(dāng)鎖被占用后,其他線程只能等待。因此,synchronized語(yǔ)義就要求線程在訪問(wèn)讀寫共享變量時(shí)只能“串行”執(zhí)行,因此synchronized具有有序性。
volatile
在java內(nèi)存模型中說(shuō)過(guò),為了性能優(yōu)化,編譯器和處理器會(huì)進(jìn)行指令重排序;也就是說(shuō)java程序天然的有序性可以總結(jié)為:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個(gè)線程觀察另一個(gè)線程,所有的操作都是無(wú)序的。在單例模式的實(shí)現(xiàn)上有一種雙重檢驗(yàn)鎖定的方式(Double-checked Locking)。代碼如下:
public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
這里為什么要加volatile了?我們先來(lái)分析一下不加volatile的情況,有問(wèn)題的語(yǔ)句是這條:
instance = new Singleton();
這條語(yǔ)句實(shí)際上包含了三個(gè)操作:1.分配對(duì)象的內(nèi)存空間;2.初始化對(duì)象;3.設(shè)置instance指向剛分配的內(nèi)存地址。但由于存在重排序的問(wèn)題,可能有以下的執(zhí)行順序:

如果2和3進(jìn)行了重排序的話,線程B進(jìn)行判斷if(instance==null)時(shí)就會(huì)為true,而實(shí)際上這個(gè)instance并沒(méi)有初始化成功,顯而易見(jiàn)對(duì)線程B來(lái)說(shuō)之后的操作就會(huì)是錯(cuò)得。而用volatile修飾的話就可以禁止2和3操作重排序,從而避免這種情況。volatile包含禁止指令重排序的語(yǔ)義,其具有有序性。
4. 可見(jiàn)性
可見(jiàn)性是指當(dāng)一個(gè)線程修改了共享變量后,其他線程能夠立即得知這個(gè)修改。通過(guò)之前對(duì)synchronzed內(nèi)存語(yǔ)義進(jìn)行了分析,當(dāng)線程獲取鎖時(shí)會(huì)從主內(nèi)存中獲取共享變量的最新值,釋放鎖的時(shí)候會(huì)將共享變量同步到主內(nèi)存中。從而,synchronized具有可見(jiàn)性。同樣的在volatile分析中,會(huì)通過(guò)在指令中添加lock指令,以實(shí)現(xiàn)內(nèi)存可見(jiàn)性。因此, volatile具有可見(jiàn)性
5. 總結(jié)
通過(guò)這篇文章,主要是比較了synchronized和volatile在三條性質(zhì):原子性,可見(jiàn)性,以及有序性的情況,歸納如下:
synchronized: 具有原子性,有序性和可見(jiàn)性;
volatile:具有有序性和可見(jiàn)性