0 前言
在并發(fā)編程中,需要解決兩個關(guān)鍵問題:
- 線程之間如何通信;
- 線程之間如何同步;
線程通信是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內(nèi)存和消息傳遞。
在共享內(nèi)存的并發(fā)模型里,線程之間共享程序的公共狀態(tài),線程之間通過寫-讀內(nèi)存中的公共狀態(tài)來隱式進行通信。
在消息傳遞的并發(fā)模型里,線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息來顯式進行通信。
線程同步是指程序用于控制不同線程之間操作發(fā)生相對順序的機制。
在共享內(nèi)存的并發(fā)模型里,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行。
在消息傳遞的并發(fā)模型里,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進行的。
Java的并發(fā)采用的是共享內(nèi)存模型,Java線程之間的通信總是隱式進行,整個通信過程對程序員完全透明 — 隱式通信、顯示同步。如果你想設(shè)計表現(xiàn)良好的并發(fā)程序,理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型規(guī)定了如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。
1 為什么要有內(nèi)存模型
在介紹Java內(nèi)存模型之前,先來看一下到底什么是計算機內(nèi)存模型,然后再來看Java內(nèi)存模型在計算機內(nèi)存模型的基礎(chǔ)上做了哪些事情。要說計算機的內(nèi)存模型,就要說一下一段古老的歷史,看一下為什么要有內(nèi)存模型。
1.1 CPU和緩存一致性
我們應(yīng)該都知道,計算機在執(zhí)行程序的時候,每條指令都是在CPU中執(zhí)行的,而執(zhí)行的時候,又免不了要和數(shù)據(jù)打交道。而計算機上面的數(shù)據(jù),是存放在主存當中的,也就是計算機的物理內(nèi)存啦。
剛開始,還相安無事的,但是隨著CPU技術(shù)的發(fā)展,CPU的執(zhí)行速度越來越快。而由于內(nèi)存的技術(shù)并沒有太大的變化,所以從內(nèi)存中讀取和寫入數(shù)據(jù)的過程和CPU的執(zhí)行速度比起來差距就會越來越大,這就導(dǎo)致CPU每次操作內(nèi)存都要耗費很多等待時間。
這就像一家創(chuàng)業(yè)公司,剛開始,創(chuàng)始人和員工之間工作關(guān)系其樂融融,但是隨著創(chuàng)始人的能力和野心越來越大,逐漸和員工之間出現(xiàn)了差距,普通員工原來越跟不上CEO的腳步。老板的每一個命令,傳到到基層員工之后,由于基層員工的理解能力、執(zhí)行能力的欠缺,就會耗費很多時間。這也就無形中拖慢了整家公司的工作效率。
可是,不能因為內(nèi)存的讀寫速度慢,就不發(fā)展CPU技術(shù)了吧,總不能讓內(nèi)存成為計算機處理的瓶頸吧。
所以,人們想出來了一個好的辦法,就是在CPU和內(nèi)存之間增加高速緩存。緩存的概念大家都知道,就是保存一份數(shù)據(jù)拷貝。它的特點是速度快,內(nèi)存小,并且昂貴。
那么,程序的執(zhí)行過程就變成了:當程序在運行過程中,會將運算需要的數(shù)據(jù)從主存復(fù)制一份到CPU的高速緩存當中,那么CPU進行計算時就可以直接從它的高速緩存讀取數(shù)據(jù)和向其中寫入數(shù)據(jù),當運算結(jié)束之后,再將高速緩存中的數(shù)據(jù)刷新到主存當中。
之后,這家公司開始設(shè)立中層管理人員,管理人員直接歸CEO領(lǐng)導(dǎo),領(lǐng)導(dǎo)有什么指示,直接告訴管理人員,然后就可以去做自己的事情了。管理人員負責(zé)去協(xié)調(diào)底層員工的工作。因為管理人員是了解手下的人員以及自己負責(zé)的事情的。所以,大多數(shù)時候,公司的各種決策,通知等,CEO只要和管理人員之間溝通就夠了。
而隨著CPU能力的不斷提升,一層緩存就慢慢的無法滿足要求了,就逐漸的衍生出多級緩存。
按照數(shù)據(jù)讀取順序和與CPU結(jié)合的緊密程度,CPU緩存可以分為一級緩存(L1),二級緩存(L3),部分高端CPU還具有三級緩存(L3),每一級緩存中所儲存的全部數(shù)據(jù)都是下一級緩存的一部分。這三種緩存的 技術(shù)難度和制造成本是相對遞減的,所以其容量也是相對遞增的。
那么,在有了多級緩存之后,程序的執(zhí)行就變成了:當CPU要讀取一個數(shù)據(jù)時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內(nèi)存中查找。
隨著公司越來越大,老板要管的事情越來越多,公司的管理部門開始改革,開始出現(xiàn)高層,中層,底層等管理者。一級一級之間逐層管理。
單核CPU只含有一套L1,L2,L3緩存。如果CPU含有多個核心,即多核CPU,則每個核心都含有一套L1(甚至和L2)緩存,而共享L3(或者和L2)緩存。
公司也分很多種,有些公司只有一個大Boss,他一個人說了算。但是有些公司有比如聯(lián)席總經(jīng)理、合伙人等機制。
單核CPU就像一家公司只有一個老板,所有命令都來自于他,那么就只需要一套管理班底就夠了。
多核CPU就像一家公司是由多個合伙人共同創(chuàng)辦的,那么,就需要給每個合伙人都設(shè)立一套供自己直接領(lǐng)導(dǎo)的高層管理人員,多個合伙人共享使用的是公司的底層員工。
還有的公司,不斷壯大,開始差分出各個子公司。各個子公司就是多個CPU了,互相之前沒有共用的資源?;ゲ挥绊?。

隨著計算機能力不斷提升,開始支持多線程。那么問題就來了。我們分別來分析下單線程、多線程在單核CPU、多核CPU中的影響。
單線程:CPU核心的緩存只被一個線程訪問。緩存獨占,不會出現(xiàn)訪問沖突等問題。
單核CPU,多線程:進程中的多個線程會同時訪問進程中的共享數(shù)據(jù),CPU將某塊內(nèi)存加載到緩存后,不同線程在訪問相同的物理地址的時候,都會映射到相同的緩存位置,這樣即使發(fā)生線程的切換,緩存仍然不會失效。但由于任何時刻只能有一個線程在執(zhí)行,因此不會出現(xiàn)緩存訪問沖突。
多核CPU,多線程:每個核都至少有一個L1 緩存。多個線程訪問進程中的某個共享內(nèi)存,且這多個線程分別在不同的核心上執(zhí)行,則每個核心都會在各自的caehe中保留一份共享內(nèi)存的緩沖。由于多核是可以并行的,可能會出現(xiàn)多個線程同時寫各自的緩存的情況,而各自的cache之間的數(shù)據(jù)就有可能不同。
在CPU和主存之間增加緩存,在多線程場景下就可能存在緩存一致性問題,也就是說,在多核CPU中,每個核的自己的緩存中,關(guān)于同一個數(shù)據(jù)的緩存內(nèi)容可能不一致。
如果這家公司的命令都是串行下發(fā)的話,那么就沒有任何問題。
如果這家公司的命令都是并行下發(fā)的話,并且這些命令都是由同一個CEO下發(fā)的,這種機制是也沒有什么問題。因為他的命令執(zhí)行者只有一套管理體系。
如果這家公司的命令都是并行下發(fā)的話,并且這些命令是由多個合伙人下發(fā)的,這就有問題了。因為每個合伙人只會把命令下達給自己直屬的管理人員,而多個管理人員管理的底層員工可能是公用的。
比如,合伙人1要辭退員工a,合伙人2要給員工a升職,升職后的話他再被辭退需要多個合伙人開會決議。兩個合伙人分別把命令下發(fā)給了自己的管理人員。合伙人1命令下達后,管理人員a在辭退了員工后,他就知道這個員工被開除了。而合伙人2的管理人員2這時候在沒得到消息之前,還認為員工a是在職的,他就欣然的接收了合伙人給他的升職a的命令。

1.2 處理器優(yōu)化和指令重排
上面提到在CPU和主存之間增加緩存,在多線程場景下會存在緩存一致性問題。除了這種情況,還有一種硬件問題也比較重要。那就是為了使處理器內(nèi)部的運算單元能夠盡量的被充分利用,處理器可能會對輸入代碼進行亂序執(zhí)行處理。這就是處理器優(yōu)化。
除了現(xiàn)在很多流行的處理器會對代碼進行優(yōu)化亂序處理,很多編程語言的編譯器也會有類似的優(yōu)化,比如:Java虛擬機的即時編譯器(JIT)也會做指令重排。
可想而知,如果任由處理器優(yōu)化和編譯器對指令重排的話,就可能導(dǎo)致各種各樣的問題。
關(guān)于員工組織調(diào)整的情況,如果允許人事部在接到多個命令后進行隨意拆分亂序執(zhí)行或者重排的話,那么對于這個員工以及這家公司的影響是非常大的。
1.3 并發(fā)編程的問題
前面說的和硬件有關(guān)的概念你可能聽得有點蒙,還不知道他到底和軟件有啥關(guān)系。但是關(guān)于并發(fā)編程的問題你應(yīng)該有所了解,比如:原子性問題,可見性問題和有序性問題。
其實,原子性問題,可見性問題和有序性問題,是人們抽象定義出來的。而這個抽象的底層問題就是前面提到的 緩存一致性問題、處理器優(yōu)化問題和指令重排問題 等。緩存一致性問題其實就是可見性問題。而處理器優(yōu)化是可以導(dǎo)致原子性問題的。指令重排即會導(dǎo)致有序性問題。
原子性 是指在一個操作中就是CPU不可以在中途暫停然后再調(diào)度,既不被中斷操作,要不執(zhí)行完成,要不就不執(zhí)行。
可見性 是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性 是指程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。
2 什么是內(nèi)存模型
前面提到的,緩存一致性問題、處理器器優(yōu)化的指令重排問題是硬件的不斷升級導(dǎo)致的。那么,有沒有什么機制可以很好的解決上面的這些問題呢?
最簡單直接的做法就是廢除處理器和處理器的優(yōu)化技術(shù)、廢除CPU緩存,讓CPU直接和主存交互。但是,這么做雖然可以保證多線程下的并發(fā)問題。但是,這就有點因噎廢食了。
所以,為了保證并發(fā)編程中可以滿足原子性、可見性及有序性。有一個重要的概念,那就是——內(nèi)存模型,定義了共享內(nèi)存系統(tǒng)中多線程程序讀寫操作行為的規(guī)范。
通過這些規(guī)則來規(guī)范對內(nèi)存的讀寫操作,從而保證指令執(zhí)行的正確性。它與處理器有關(guān)、與緩存有關(guān)、與并發(fā)有關(guān)、與編譯器也有關(guān)。它解決了CPU多級緩存、處理器優(yōu)化、指令重排等導(dǎo)致的內(nèi)存訪問問題,保證了并發(fā)場景下的一致性、原子性和有序性。
內(nèi)存模型解決并發(fā)問題主要采用兩種方式:限制處理器優(yōu)化和使用內(nèi)存屏障。
3 什么是Java內(nèi)存模型
前面介紹過了計算機內(nèi)存模型,這是解決多線程場景下并發(fā)問題的一個重要規(guī)范。那么具體的實現(xiàn)是如何的呢,不同的編程語言,在實現(xiàn)上可能有所不同。
我們知道,Java程序是需要運行在Java虛擬機上面的,Java內(nèi)存模型(Java Memory Model ,JMM)就是一種符合內(nèi)存模型規(guī)范的,屏蔽了各種硬件和操作系統(tǒng)的訪問差異的,保證了Java程序在各種平臺下對內(nèi)存的訪問都能保證效果一致的機制及規(guī)范。
Java內(nèi)存模型規(guī)定了 所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程中用到的變量的主內(nèi)存副本拷貝,線程對變量的所有操作都必須在工作內(nèi)存中進行,而不能直接讀寫主內(nèi)存。不同的線程之間也無法直接訪問對方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進行數(shù)據(jù)同步進行。
而JMM就作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程。他規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步。

特別需要注意的是,主內(nèi)存和工作內(nèi)存與JVM內(nèi)存結(jié)構(gòu)中的Java堆、棧、方法區(qū)等并不是同一個層次的內(nèi)存劃分,無法直接類比。
再來總結(jié)下,JMM是一種規(guī)范,規(guī)范了Java虛擬機與計算機內(nèi)存是如何協(xié)同工作的,目的是解決由于多線程通過共享內(nèi)存進行通信時,存在的本地內(nèi)存數(shù)據(jù)不一致、編譯器會對代碼指令重排序、處理器會對代碼亂序執(zhí)行等帶來的問題。目的是保證并發(fā)編程場景中的原子性、可見性和有序性。
所以,如果你想設(shè)計表現(xiàn)良好的并發(fā)程序,理解Java內(nèi)存模型是非常重要的。Java內(nèi)存模型規(guī)定了如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。
3.1 Java內(nèi)存模型抽象
在Java中,所有實例域、靜態(tài)域和數(shù)組元素存儲在堆內(nèi)存中,堆內(nèi)存在線程之間共享。局部變量(Local variables),方法定義參數(shù)(formal method parameters)和異常處理器參數(shù)(exception handler parameters)不會在線程之間共享,它們不會有內(nèi)存可見性問題,也不受內(nèi)存模型的影響。
Java線程之間的通信由Java內(nèi)存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:
線程之間的共享變量存儲在主內(nèi)存(main memory)中,每個線程都有一個私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區(qū),寄存器以及其他的硬件和編譯器優(yōu)化。

從上圖來看,線程A與線程B之間如要通信的話,必須要經(jīng)歷下面2個步驟:
- 首先,線程A把本地內(nèi)存A中更新過的共享變量刷新到主內(nèi)存中去;
- 然后,線程B到主內(nèi)存中去讀取線程A之前已更新過的共享變量;
下面通過示意圖來說明這兩個步驟:

如上圖所示,本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本。假設(shè)初始時,這三個內(nèi)存中的x值都為0。線程A在執(zhí)行時,把更新后的x值(假設(shè)值為1)臨時存放在自己的本地內(nèi)存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時主內(nèi)存中的x值變?yōu)榱?。隨后,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時線程B的本地內(nèi)存的x值也變?yōu)榱?。
從整體來看,這兩個步驟實質(zhì)上是線程A在向線程B發(fā)送消息,而且這個通信過程必須要經(jīng)過主內(nèi)存。JMM通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互,來為Java程序員提供內(nèi)存可見性保證。
3.2 重排序
在執(zhí)行程序時為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分三種類型:
- 編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
- 指令級并行的重排序?,F(xiàn)代處理器采用了指令級并行技術(shù)(Instruction-Level Parallelism, ILP)來將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機器指令的執(zhí)行順序。
- 內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲操作看上去可能是在亂序執(zhí)行。
從Java源代碼到最終實際執(zhí)行的指令序列,會分別經(jīng)歷下面三種重排序:

上述的1屬于編譯器重排序,2和3屬于處理器重排序。這些重排序都可能會導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見性問題。
對于編譯器,JMM的編譯器重排序規(guī)則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
對于處理器重排序,JMM的處理器重排序規(guī)則會要求Java編譯器在生成指令序列時,插入特定類型的內(nèi)存屏障(memory barriers,intel稱之為memory fence)指令,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)。
JMM屬于語言級的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證。
3.3 處理器重排序
現(xiàn)代的處理器使用 寫緩沖區(qū) 來臨時保存向內(nèi)存寫入的數(shù)據(jù)。寫緩沖區(qū)可以保證指令流水線持續(xù)運行,它可以避免由于處理器停頓下來等待向內(nèi)存寫入數(shù)據(jù)而產(chǎn)生的延遲。同時,通過以批處理的方式刷新寫緩沖區(qū),以及合并寫緩沖區(qū)中對同一內(nèi)存地址的多次寫,可以減少對內(nèi)存總線的占用。雖然寫緩沖區(qū)有這么多好處,但每個處理器上的寫緩沖區(qū),僅僅對它所在的處理器可見。這個特性會對內(nèi)存操作的執(zhí)行順序產(chǎn)生重要的影響:
處理器對內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實際發(fā)生的讀/寫操作順序一致!

假設(shè)處理器A和處理器B按程序的順序并行執(zhí)行內(nèi)存訪問,最終卻可能得到 x = y = 0。具體的原因如下圖所示:

處理器 A 和 B 同時把共享變量寫入在寫緩沖區(qū)中(A1、B1),然后再從內(nèi)存中讀取另一個共享變量(A2、B2),最后才把自己寫緩沖區(qū)中保存的臟數(shù)據(jù)刷新到內(nèi)存中(A3、B3)。當以這種時序執(zhí)行時,程序就可以得到 x = y = 0 的結(jié)果。
從內(nèi)存操作實際發(fā)生的順序來看,直到處理器 A 執(zhí)行 A3 來刷新自己的寫緩存區(qū),寫操作 A1 才算真正執(zhí)行了。雖然處理器 A 執(zhí)行內(nèi)存操作的順序為:A1 -> A2,但內(nèi)存操作實際發(fā)生的順序卻是:A2 -> A1。此時,處理器 A 的內(nèi)存操作順序被重排序了。
這里的關(guān)鍵是,由于寫緩沖區(qū)僅對自己的處理器可見,它會導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會與內(nèi)存實際的操作執(zhí)行順序不一致。由于現(xiàn)代的處理器都會使用寫緩沖區(qū),因此現(xiàn)代的處理器都會允許對內(nèi)存寫-讀操作重排序。
3.4 內(nèi)存屏障指令
下面是常見處理器允許的重排序類型的列表:

上表單元格中的“N”表示處理器不允許兩個操作重排序,“Y”表示允許重排序。從上表我們可以看出:常見的處理器都允許Store-Load重排序;常見的處理器都不允許對存在數(shù)據(jù)依賴的操作做重排序。sparc-TSO和x86擁有相對較強的處理器內(nèi)存模型,它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區(qū))。
為了保證內(nèi)存可見性,Java 編譯器在生成指令序列的適當位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。JMM 把內(nèi)存屏障指令分為下列四類:

3.5 happens-before
JSR-133 內(nèi)存模型使用 happens-before 的概念來闡述操作之間的內(nèi)存可見性。在 JMM 中,如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在 happens-before 關(guān)系。這里提到的兩個操作既可以是在一個線程之內(nèi),也可以是在不同線程之間。
與程序員密切相關(guān)的 happens-before 規(guī)則如下:
程序順序規(guī)則:一個線程中的每個操作,happens- before 于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對一個監(jiān)視器鎖的解鎖,happens- before 于隨后對這個監(jiān)視器鎖的加鎖。
volatile變量規(guī)則:對一個volatile域的寫,happens- before 于任意后續(xù)對這個volatile域的讀。
傳遞性規(guī)則:如果A happens- before B,且B happens- before C,那么A happens- before C。
注意,兩個操作之間具有happens-before關(guān)系,并不意味著前一個操作必須要在后一個操作之前執(zhí)行!happens-before僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。

如上圖所示,一個happens-before規(guī)則通常對應(yīng)于多個編譯器和處理器重排序規(guī)則。對于Java程序員來說,happens-before規(guī)則簡單易懂,它避免java程序員為了理解JMM提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)。
3.6 數(shù)據(jù)依賴性
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數(shù)據(jù)依賴性。數(shù)據(jù)依賴分下列三種類型:

上面三種情況,只要重排序兩個操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會被改變。
前面提到過,編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。
注意,這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
3.7 as-if-serial 語義
as-if-serial 語義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)果不能被改變。編譯器,runtime 和處理器都必須遵守 as-if-serial 語義。
為了遵守 as-if-serial 編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因為這種重排序會改變執(zhí)行結(jié)果。但是如果操作之間沒有數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
舉個例子:
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
上面三個操作的數(shù)據(jù)依賴關(guān)系如下圖所示:

如上圖所示,A 和 C 之間存在數(shù)據(jù)依賴關(guān)系,同時 B 和 C 之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的結(jié)果將會被改變)。但 A 和 B 之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序 A 和 B 之間的執(zhí)行順序。下圖是該程序的兩種可能執(zhí)行順序:

在計算機中,軟件技術(shù)和硬件技術(shù)有一個共同的目標:在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開發(fā)并行度。編譯器和處理器遵從這一目標,從 happens-before 的定義我們可以看出,JMM 同樣遵從這一目標。
4 Java內(nèi)存模型實現(xiàn)
了解Java多線程的朋友都知道,在Java中提供了一系列和并發(fā)處理相關(guān)的關(guān)鍵字,比如:volatile、synchronized、final、concurrent包等。其實這些就是 Java內(nèi)存模型封裝了底層的實現(xiàn)后提供給程序員使用的一些關(guān)鍵字。
在開發(fā)多線程的代碼的時候,我們可以直接使用 synchronized 等關(guān)鍵字來控制并發(fā),從來就不需要關(guān)心底層的編譯器優(yōu)化、緩存一致性等問題。所以,Java內(nèi)存模型,除了定義了一套規(guī)范,還提供了一系列原語,封裝了底層實現(xiàn)后,供開發(fā)者直接使用。
4.1 原子性
在Java中,為了保證原子性,提供了兩個高級的字節(jié)碼指令 monitorenter 和 monitorexit,這兩個字節(jié)碼,在Java中對應(yīng)的關(guān)鍵字就是 synchronized。
因此,在Java中可以使用 synchronized 來保證方法和代碼塊內(nèi)的操作是原子性的。
4.2 可見性
Java內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值的這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)的。
Java中的 volatile 關(guān)鍵字提供了一個功能,那就是被其修飾的變量在被修改后可以立即同步到主內(nèi)存,被其修飾的變量在每次是用之前都從主內(nèi)存刷新。因此,可以使用 volatile 來保證多線程操作時變量的可見性。
除了 volatile,Java中的 synchronized 和 final 兩個關(guān)鍵字也可以實現(xiàn)可見性。只不過實現(xiàn)方式不同,這里不再展開了。
4.3 有序性
在Java中,可以使用 synchronized 和 volatile 來保證多線程之間操作的有序性。實現(xiàn)方式有所區(qū)別:
volatile關(guān)鍵字會禁止指令重排;
synchronized關(guān)鍵字保證同一時刻只允許一條線程操作;