日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java内存模型、volatile、原子性、可见性、有序性、happens-before原则

發(fā)布時間:2024/10/14 java 131 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java内存模型、volatile、原子性、可见性、有序性、happens-before原则 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

目錄

1.硬件的效率與一致性:

緩存一致性(Cache Coherence)

2.Java內(nèi)存模型

2.1主內(nèi)存與工作內(nèi)存

2.2內(nèi)存間的交互

2.3 volatile型變量的特殊規(guī)則

2.3.1 保證此變量對所有線程的可見性;

2.3.2 禁止指令重排序優(yōu)化

2.3.4 在volatile與鎖之中選擇的唯一依據(jù)

2.3.4 JMM中對volatile變量定義的特殊規(guī)則

2.4對于double和long型變量的特殊規(guī)則

2.5 原子性、可見性和有序性

2.5.1原子性atomicity

2.5.2 可見性visibility

2.5.3 有序性 ordering

2.6 先行發(fā)生原則happens-before

JMM的天然的先行發(fā)生關(guān)系


并發(fā)處理的廣泛應用是使得Amdahl定律代替摩爾定律成為計算機性能發(fā)展原動力的根本原因。

多任務處理:讓計算機同時去做幾件事情。

  • 很重要的原因:計算機的運算速度與它的存儲和通信子系統(tǒng)虛度差距太大,大量的時間都花費在磁盤I/O、網(wǎng)絡通信或數(shù)據(jù)庫訪問上。

每秒事務處理數(shù)(transacrions per seconds,TPS):衡量一個服務性能的高低好壞。

  • 它代表著一秒內(nèi)服務端平均能響應的請求總數(shù)
  • 與并發(fā)能力有很大關(guān)系:
    • 計算量相同的任務,程序線程并發(fā)協(xié)調(diào)得有條不紊,效率自然會高;
    • 線程之間頻繁發(fā)生阻塞甚至死鎖,將會大大降低程序的并發(fā)能力。

1. 硬件的效率與一致性:

物理計算機中的并發(fā):

  • 執(zhí)行任務:處理器至少要完成與內(nèi)存交互,如讀取數(shù)據(jù)、存儲運算結(jié)果等。
  • 這個I/O操作很難消除。
  • 計算機的存儲設備與處理器的運算速度由幾個數(shù)量及的差距,所以現(xiàn)代計算機系統(tǒng)都加入了一層讀寫速度盡可能接近處理器運算速度的高速緩存(cache)來作為內(nèi)存與處理器之間的緩沖;
    • 將運算需要使用到的數(shù)據(jù)復制到緩存中,讓運算能快速進行;
    • 當運算結(jié)束后,再從緩存同步到內(nèi)存中,
    • 這樣處理器就無需等待緩慢的內(nèi)存讀寫了。

基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾,但也引入了一個新問題:

緩存一致性(Cache Coherence)

百度百科:又譯為緩存連貫性緩存同調(diào),是指保留在高速緩存中的共享資源,保持數(shù)據(jù)一致性的機制。

在多處理器系統(tǒng)中,每個處理器都有自己的高速緩存,而它們又共享同一主內(nèi)存(Main Memory),如圖所示:

當多個處理器的運算任務都涉及同一塊主內(nèi)存區(qū)域時,將可能導致各自的緩存數(shù)據(jù)不一致。

這就需要各個處理器訪問緩存時都遵循一些協(xié)議:MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等。

內(nèi)存模型:在特定操作協(xié)議下,對特定的內(nèi)存或者告訴緩存進行讀寫訪問的過程抽象。

  • 不同架構(gòu)可以擁有不一樣的內(nèi)存模型
  • JVM也有自己的內(nèi)存模型。

除了增加高速緩存以外,為了使得處理器內(nèi)部的運算單元盡量被充分利用,處理器可能會對輸入代碼進行亂序執(zhí)行(out-of-order execution)優(yōu)化處理器會在計算之后將亂序的執(zhí)行結(jié)果重組,保證該結(jié)果與順序執(zhí)行的結(jié)果是一致的,但并不保證程序各個語句計算的先后順序與輸入代碼中的順序一致。

所以——如果存在一個計算任務依賴另外一個計算任務的中間結(jié)果,那么其順序性并不能靠代碼的先后順序保證。

JVM的即時編譯器中也有和亂序執(zhí)行優(yōu)化類似的指令重排序(instruction reorder)優(yōu)化

2. Java內(nèi)存模型

JMM,來屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓Java程序在各個平臺下都能達到一致的內(nèi)存訪問效果。

2.1 主內(nèi)存與工作內(nèi)存

JMM的主要目標是定義程序中各個變量的訪問規(guī)則,即在虛擬機中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細節(jié)。

  • 變量(variables):包括實例字段、靜態(tài)字段和構(gòu)成數(shù)組對象的元素,但不包括局部變量與方法參數(shù)。
  • 因為后面的是線程私有的,不會被共享。

為了獲得較高的執(zhí)行效能,JMM并沒有限制執(zhí)行引擎適用處理器的特定寄存器或緩存來和主內(nèi)存進行交互,也沒有限制即時編譯器進行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。

JMM規(guī)定,所有的變量都存儲在主內(nèi)存(Main memory)(可類比,物理硬件是的主內(nèi)存,虛擬機內(nèi)存中的一部分)。

每條線程有自己的工作內(nèi)存(working memory)(可類比。處理器高速緩存)。

  • 線程的工作內(nèi)存保存了被該線程使用到的變量的主內(nèi)存副本拷貝。
  • 線程對變量的所有操作(讀取、賦值等)必須在工作內(nèi)存中進行,而不能把自己接讀寫主內(nèi)存中的變量。
  • 不同線程之間也無法直接訪問對方的工作內(nèi)存中的變量。
  • 線程間變量值的傳遞均需要通過主內(nèi)存來完成。

線程、主內(nèi)存、工作內(nèi)存三者之間的交互關(guān)系如圖:

這里所提到的主內(nèi)存、工作內(nèi)存與Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個層次的內(nèi)存劃分。基本上沒有聯(lián)系。

2.2 內(nèi)存間的交互

JMM中定義了一下8種操作來完成主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議(即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存,如何從工作內(nèi)存同步會主內(nèi)存之類的實現(xiàn)細節(jié)),虛擬機必須保證下面提及的每一種操作都是原子的、不可再分的(對于double和long類型的變量來說,load、store、read和write操作在某些平臺商允許有例外):

  • lock(鎖定):作用于主內(nèi)存的變量,它把一個變量標識為一條線程獨占的狀態(tài);
  • unlock(解鎖):作用于主內(nèi)存的變量,它吧一個處于鎖定狀態(tài)的對變量釋放出來,釋放后的變量才可以被其它線程鎖定;
  • read(讀取):作用于主內(nèi)存的變量,它把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)浆F(xiàn)成的工作內(nèi)存中,以便隨后的load動作使用;
  • load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中;
  • use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳給執(zhí)行引擎,每當虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將會執(zhí)行這個操作;
  • assign(賦值):作用于工作內(nèi)存的變量,它把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量,每當虛擬機遇到一個黑變量賦值的字節(jié)碼指令時執(zhí)行這個操作;
  • store(存儲):作用于工作內(nèi)存的變量,它把工作內(nèi)存中一個變量的值傳送到主內(nèi)存中,以便隨后的write操作使用;
  • write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的白能量的值放入主內(nèi)存的變量中。
  • 除此之外,JMM還規(guī)定了在執(zhí)行者8種基本操作必須滿足如下規(guī)則:

  • 在把一個變量從主內(nèi)存復制到工作內(nèi)存時,要順序執(zhí)行read和load操作;反之則要順序執(zhí)行store和write操作;
  • 不是連續(xù),read和load之間、store和write可以插入其他指令
  • 不允許read和load、store和write操作之一單獨出現(xiàn),即不允許一個變量從主內(nèi)存讀取了但工作內(nèi)存不接受,或者從工作內(nèi)存發(fā)起回寫了但是主內(nèi)存不接受的情況出現(xiàn);
  • 不允許一個線程丟棄它的最近的assign操作,即變量在工作內(nèi)存中改變了之后必須把該改變同步到主內(nèi)存;
  • 不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從線程的工作內(nèi)存同步回主內(nèi)存中;
  • 一個新的變量只能在主內(nèi)存中“誕生”,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量,即就是對一個變量實施use、store操作之前,必須先執(zhí)行過了assign和load操作;
  • 一個變量在同一時刻只允許一條線程對其進行操作lock操作,但lock操作可以被同一條線程重復執(zhí)行多次,多次執(zhí)行l(wèi)ock后,只有執(zhí)行相同次數(shù)的unlock操作,變量才會被解鎖;
  • 如果對一個變量執(zhí)行l(wèi)ock操作,那將會清空工作內(nèi)存中此變量的值,在執(zhí)行引擎使用這個變量前,需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值;
  • 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執(zhí)行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量;
  • 對一個變量執(zhí)行unlock之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)。
  • 2.3 volatile型變量的特殊規(guī)則

    關(guān)鍵字volatile是JVM提供的最輕量級的同步機制

    當一個變量定義為volatile之后,它將具備兩種特性:

    2.3.1 保證此變量對所有線程的可見性

    • 可見性:指當一條線程修改了這個變量的值,新值對于其他線程來說是可以立即得知的
    • 而普通變量的值在線程間傳遞需要通過主內(nèi)存來完成。
    • volatile變量對所有線程是立即可見的,對volatile變量的所有的寫操作都能立即反映到其它線程之中,即volatile變量在各個線程中是一致的。
    • 但是并不能得出“基于volatile變量的運算在并發(fā)下是安全的”。
    • volatile變量在各個線程的工作內(nèi)存中不存在一致性問題(在各個線程的工作內(nèi)存中,volatile變量也可以存在不一致的情況,但由于每次使用之前都需要刷新,執(zhí)行引擎可以看到不一致的情況,因此可以認為不存在一致性問題)。
    • 但在Java里面的運算并非原子操作,導致volatile變量的運算在并發(fā)下一樣是不安全的。如下代碼演示了可說明原因:
    /* *volatile變量自增運算的測試 */ public class Test{public static volatile int race=0;public static void increase() {race++;}public static final int THREADS_COUNT=20;public static void main(String[] args) {Thread[] threads=new Thread[THREADS_COUNT];for(int i=0;i<THREADS_COUNT;i++) {threads[i]=new Thread(new Runnable() {@Overridepublic void run() {for(int i=0;i<10000;i++) {increase();}}});threads[i].start();}//等待所有線程都結(jié)束while(Thread.activeCount()>1)Thread.yield();System.out.println(race);} }

    運行結(jié)果:

    這段代碼發(fā)起了20個線程,每個線程對race變量進行1000次自增操作。

    如果這段代碼能夠正確并發(fā)的話,最后輸出的結(jié)果應該是200000.

    但運行后并不會得到期望的結(jié)果,總小于200000.

    問題:自增運算race++

    使用Javap反編譯這段代碼后,得到:

    public static void increase();Code:Stack=2,Locals=0,Args_size=00: getstatic #13;//Field race:I3: iconst_14: iadd5:putstatic #13;//Field race:I8: return LineNumberTable:line 14:0line 15:8

    只有一行代碼的increase()方法在Class文件中是由4字節(jié)碼指令構(gòu)成的(return 不是有race++產(chǎn)生的,這條指令可以不計算),從字節(jié)碼層面上很容易就分析出并發(fā)失敗的原因了:

    • 當getstatic指令把race的值取到操作棧頂時,volatile關(guān)鍵字保證了race的值此時是正確的;
    • 但是在執(zhí)行iconst_1、iadd這些指令的時候,其它線程可能已經(jīng)把race的值加大了,而在操作棧頂?shù)闹稻妥兂闪诉^起的數(shù)據(jù),所以putstatic指令執(zhí)行后就可能把較小的race值同步回主內(nèi)存中。

    即使編譯出來只有一條字節(jié)碼指令,也并不意味著執(zhí)行這條指令就是一個原子操作。

    • 一條字節(jié)碼指令在解釋執(zhí)行時,解釋器將要運行許多行代碼才能實現(xiàn)它的語義,如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉(zhuǎn)化為若干條本地機器指令,此處使用-XX:+PrintAssembly參數(shù)輸出反匯編來分析會更嚴謹些。

    由于volatile變量只能保證可見性,在不符合以下兩條規(guī)則的運算場景中,仍需要通過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性:

    某個變量定義為volatile的應用場景:

    • 運算結(jié)果并不依賴變量的當前值,或者能夠確保自由單一的線程修改變量的值;
    • 變量不需要與其他的狀態(tài)變量共同參與不變約束。

    很適合使用volatile變量來控制并發(fā)的場景:

    volatile boolean shutdownRequested; public void shutdown() {shutdownRequested=true; } public void dowork() {while(!shutdownRequested) {//do stuff} }

    當shutdown()方法被調(diào)用時,能保證所有線程中執(zhí)行的dowork()方法都立即停下來。

    2.3.2 禁止指令重排序優(yōu)化

    普通的變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲取到正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致。——線程內(nèi)表現(xiàn)為串行的語義(within-thread as-if-serial semantics)。

    如下例子展示了為何指令重排序會干擾并發(fā)的執(zhí)行:

    Map configOptions; char[] configText; //此變量必須定義為volatile volatile boolean initialized=false;//假設以下代碼在線程A中執(zhí)行 //模擬讀取配置信息,當讀取完成后將initialized設置為true以通知其它線程配置可用 configOptions=new HashMap(); configTest=readConfigFile(fileName); processConfigOptions(configText,configOptions); initialized=true;//假設以下代碼在線程B中執(zhí)行 //等待initialized為true,代表線程A已經(jīng)把配置信息初始化完成; while(!initialized) {sleep(); } //使用線程A中初始化好的配置信息 doSomethingWithConfig();

    如果initialized變量沒有用volatile修飾,就可能會由于指令重排序的優(yōu)化,導致位于線程A的最后一句代碼initialized=true被提前執(zhí)行(這里雖然使用Java作為偽代碼,但所指的重排序優(yōu)化是機器及的優(yōu)化操作,提前執(zhí)行是指這句話對應的匯編代碼被提前執(zhí)行),這樣,在線程B中使用配置信息的代碼就可能出現(xiàn)錯誤,而volatile關(guān)鍵字則可以避免此類情況的發(fā)生。

    一下代碼分析了volatile關(guān)鍵字是如何禁止指令重排序優(yōu)化的:

    public class Singleton{private volatile static Singleton instance;public static Singleton getInstance() {if(instance==null) {synchronized(Singleton.class) {if(instance==null) {instance=new Singleton();}}}return instance;}public static void main(String[] args) {Singleton.getInstance();} }

    編譯后,這段代碼對instance變量復制部分如下所示:

    關(guān)鍵變化在于由volatile修飾前面mov%eax,0x150(%esi)這句便是賦值操作)多執(zhí)行了一個“l(fā)ock addl $ 0x0, (%esp)"操作,這個操作相當于一個內(nèi)存屏障(memory barrier或memory fence,指重排序是不能把后面的指令重排序到內(nèi)存屏障之前的位置),只有一個CPU訪問內(nèi)存時,并不需要內(nèi)存屏障;

    但如果有兩個或更多CPU訪問同一塊內(nèi)存,且其中有一個在觀測另一個,就需要內(nèi)存屏障來保證一致性了。

    addl $ 0x0, (%esp)這句指令,把ESP寄存器的值加0,顯然是一個空操作。采用這個空操作而不是空操作指令nop是因為IA32手冊規(guī)定lock前綴不允許配合nop指令使用

    關(guān)鍵在于Lock前綴。它的作用是使得本CPU的Cache寫入了內(nèi)存,該寫入動作也會引起別的CPU或者別的內(nèi)核無效化(Invalidae)其cache,這種操作相當于對cache中的變量做了一次前面介紹的JMM中所說的“store和write”操作。所以通過這樣一個空操作,可以讓前面的volatile變量的修改對其他CPU立即可見。

    volatile禁止重排序:

    從硬件架構(gòu)上講,指令重排序是指CPU采用了允許將多條指令不按程序規(guī)定的順序分開發(fā)送給各個響應的電路單元處理。但并不是說,指令任意重排,CPU需要能正確處理指令以來情況以保障程序能得到正確的執(zhí)行結(jié)果。

    所以在本CPU內(nèi),重排序看起來依然是有序的。因此lock?addl $ 0x0, (%esp)指令把修改同步到內(nèi)存中,意味著之前所欲的操作都已經(jīng)執(zhí)行完成,這樣便形成了“指令重排序無法越過內(nèi)存屏障”的效果。

    volatile變量讀操作的性能消耗與普通變量幾乎沒有什么差別,但是寫操作可能會慢點,因為他需要在本地代碼中插入許多內(nèi)存屏障指令來保證處理器不發(fā)生亂序執(zhí)行。

    2.3.4 在volatile與鎖之中選擇的唯一依據(jù)

    僅僅是volatile的語義能否滿足使用場景的需求。

    2.3.4 JMM中對volatile變量定義的特殊規(guī)則

    ① 在工作內(nèi)存中每次使用變量前都需要從主內(nèi)存中刷新最新值;

    ② 每次修改變量的值之后都必須立刻同步到主內(nèi)存中;

    ③ 要求volatile修飾的變量不會被指令重新排序。

    假定T表示一個線程,V和W分別表示兩個volatile型變量。那么在進行read、load、use、assign、store和write操作時需要滿足一下規(guī)則:

    • 只有當線程T對變量V執(zhí)行的前一個動作是load的時候,T才能對V執(zhí)行use動作;并且只有當T對V執(zhí)行的后一個動作是use的時候,T才能對V執(zhí)行l(wèi)oad動作。T對V的use動作可以認為是和T對V的load、read動作相關(guān)聯(lián),必須連續(xù)一起出現(xiàn)(這條規(guī)則要求在工作內(nèi)存中,每次使用V前都必須現(xiàn)充主內(nèi)存刷新最新的值,用于保證能看見其它線程對V所做的修改后的值)。
    • 只有當T對V執(zhí)行的前一個動作是assign的時候,T才能對V執(zhí)行store動作;并且,只有當T對V執(zhí)行的后一個動作是store的時候,T才能對V執(zhí)行assign動作。T對V的assign動作可以認為是和T對V的store、write動作相關(guān)聯(lián),必須連續(xù)一起粗線(這條規(guī)則要求在工作內(nèi)存中,每次修改V后都必須立即同步回主內(nèi)存中,用于保證其他線程可以看到自己對V所做的修改)。
    • 假定動作A是T對V實施的use或assign動作,假定動作F是和動作A相關(guān)聯(lián)的load或store動作,假定動作P是和動作F相對應的對V的read或write動作;類似地,假定動作B是對變量W實施的use或assign動作,假定動作G是和動作B相關(guān)聯(lián)的load或store動作,假定動作Q是和動作G相對應的對W的read或write動作。如果A先于B,那么P先于Q(這條規(guī)則要求volatile修飾的變量不會被指令重排序優(yōu)化,保證代碼的執(zhí)行順序與程序的順序相同)。

    2.4 對于double和long型變量的特殊規(guī)則

    JMM要求lock、unlock、read、load、assign、use、store、write這8個操作都具有原子性。

    對于64位的數(shù)據(jù)類型(long和double)在模型中特別定義了一條相對較寬松的規(guī)定:

    允許虛擬機將沒有被volatile修飾的64位數(shù)據(jù)的讀寫操作劃分為兩次32位的操作來進行。即允許虛擬機實現(xiàn)選擇可以不保證64位數(shù)據(jù)類型的load、read、store和write這4個操作的原子性。——long和double的非原子性協(xié)定(Nonatomic treament of double and long variables)。

    如果有多個線程共享一個并未聲明為volatile的long或double類型的變量,并且同時對他們進行讀取和修改操作,那么某些線程可能會讀取到一個既非原值,也不是其它線程修改值的代表了“半個變量”的數(shù)值(很罕見)。

    • 因為,JMM雖然允許虛擬機不把long和double變量的讀寫實現(xiàn)為原子操作,但允許虛擬機選擇把這些操作時限為具有原子性的操作,而且還“強烈建議”虛擬機這樣實現(xiàn)。
    • 因此,在編寫代碼的時候一般不需要把用到的long和double變量專門聲明為volatile。

    2.5 原子性、可見性和有序性

    2.5.1 原子性atomicity

    由JMM來直接保證的原子性變量操作包括:

    • read
    • load
    • assign
    • use
    • store
    • write

    可以大致認為基本數(shù)據(jù)類型的訪問和讀寫是具備原子性的(例外就是long和double的非原子性協(xié)定)。

    如果應用場景中需要更大范圍的原子性保證(經(jīng)常會遇到),JMM還提供了lock和unlock操作來滿足這種需求。

    盡管虛擬機未把lock和unclock操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令monitoerenter和monitorexit來隱式地使用這兩個操作,這兩個字節(jié)碼指令反應在Java代碼中就是同步塊——synchronized關(guān)鍵字,因此在synchronized塊之間的操作也具備原子性。

    2.5.2 可見性visibility

    指當一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。

    JMM是通過在變量修改后將新值同步回主內(nèi)存中,在變量讀取前從主內(nèi)存刷新變量值這種依賴主內(nèi)存作為傳遞媒介的方式來實現(xiàn)可見性的。

    無論是普通變量還是volatile變量都是如此。

    普通變量和volatile的區(qū)別是:

    • volatile的特殊規(guī)則保證了新值能立即同步到主內(nèi)存,以及每次使用前立即從主內(nèi)存刷新。
    • 因此,volatile保證了多線程操作是變量的可見性
    • 而普通變量則不能保證這一點。

    Java還有synchronizedfinal關(guān)鍵字可以實現(xiàn)可見性。

    • 同步塊的可見性是由“對一個變量執(zhí)行unlock之前,必須先把此變量同步回主內(nèi)存中(執(zhí)行store、write操作)”這條規(guī)則獲得的。
    • final關(guān)鍵字的可見性:被final修飾的字段在構(gòu)造其中一旦初始化完成,并且構(gòu)造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事,其它線程可能通過這個引用訪問到“初始化了一半”的對象),那在其它線程中就能看見final字段的值。如下代碼:
    public static fibal int i; public final int j;static {i=0;//do something } {//也可以選擇在構(gòu)造函數(shù)中初始化j=0;//do something }

    2.5.3 有序性 ordering

    Java程序的天然有序性可以總結(jié)為一句話:如果在本線程內(nèi)觀察,所有的操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。

    • 前半句:線程內(nèi)表現(xiàn)為串行語義——within-thread as-if-serial semantics
    • 后半句:指令重排序現(xiàn)象和工作內(nèi)存與主內(nèi)存同步延遲現(xiàn)象

    Java語言提供了volatilesynchronized兩個關(guān)鍵字來保證線程之間操作的有序性

    • volatile關(guān)鍵字:本身就包含了禁止指令重排序的語義
    • synchronized:是由“一個變量在同一時刻只允許一條線成對其進行l(wèi)ock操作”這條規(guī)則獲得的。這條規(guī)則決定了持有同一個鎖的兩個同步塊只能串行的進行。

    synchronized關(guān)鍵字在這三種特性都可以作為一種解決方案。

    2.6 先行發(fā)生原則happens-before

    它是判斷數(shù)據(jù)是否存在競爭、線程是否安全的重要依據(jù)。

    它是JMM中定義的兩項操作之間的偏序關(guān)系

    • 如果說操作A先行發(fā)生于操作B,其實就是在說發(fā)生在操作B之前,操作A產(chǎn)生的影響能被操作B觀察到。
      • 影響:包括修改了內(nèi)存中的值、發(fā)送了消息、調(diào)用了方法等。
      • 舉例:
    //以下操作在線程A中執(zhí)行 i=1;//以下操作在線程B中執(zhí)行 j=i;//以下操作在線程C中執(zhí)行 i=2;

    分析:

    假設線程A的操作“i=1”先行發(fā)生于線程B的操作“j=i”,那么可以確定在線程B的操作執(zhí)行之后,變量j的值一定等于1。得出這個結(jié)論的依據(jù)有兩個:

    • 根據(jù)先行發(fā)生原則,“i=1”的結(jié)果可以被觀察到;
    • 線程C還沒登場,A操作結(jié)束后,沒有其他線程會修改變量i的值。

    再考慮線程C,依然保持A和B之間的先行發(fā)生關(guān)系,而C出現(xiàn)在A和B之間,但是C和B沒有先行發(fā)生關(guān)系,那j的值會使多少?

    答案是不確定。1和2都有可能。

    • C對i的影響可能會被B觀察到,也可能不會
    • 這時候,B就存在讀取到過期數(shù)據(jù)的風險,不具備多線程安全性。

    JMM的天然的先行發(fā)生關(guān)系

    無需任何同步器的協(xié)助就已經(jīng)存在,可在編碼中直接利用。

    如果兩個操作之間的關(guān)系不在此列,并且無法從下列推導出來的話,它們就沒有順序性保障,虛擬機可以隨意地對它們進行重排序。

    • 程序次序規(guī)則(program order rule):在一個線程內(nèi),按照程序代碼順序,書寫在前面的操作先行發(fā)生于書寫在后面的操作。準確的說,應該是控制流順序而不是程序代碼的順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
    • 管程鎖定規(guī)則(monitor lock rule):一個unlock操作先行發(fā)生于后面對同一個鎖的lock操作。這里必須強調(diào)的是同一個鎖,而“后面”是指時間上的先后順序。
    • volatile變量規(guī)則(volatile variables rule):對一個volatile變量的寫操作先行發(fā)生于后面對這個變量的讀操作。這里的“后面”是指時間上的額先后順序。
    • 線程啟動規(guī)則(thread start rule):Thread獨享的start()方法先行發(fā)生于此線程的每一個動作。
    • 線程終止規(guī)則(Thread termination rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測。可通過Thread.join()方法結(jié)束、Thread.isAlive()的返回值等手段檢測到線程已經(jīng)終止。
    • 線程中斷檢測(thread interruption rule):對縣城interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷時間的發(fā)生,可通過Thread.interrupted()方法檢測到是否有中斷發(fā)生。
    • 對象終結(jié)規(guī)則(finalizer rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的finalizer()方法的開始。
    • 傳遞性(transitivity):如果操作A先行發(fā)生于操作BB,操作B先行發(fā)生于操作C,那就可以得出操作A先行發(fā)生于操作C的結(jié)論。

    “時間上的先后順序”和“先行發(fā)生于”有何不同?

    private int value=0;public void setValue(int value){this.value=value; }public int getValue(){return value; }

    假設存在線程A和B,A先(時間上的先后)調(diào)用了setValue(1),然后B調(diào)用了同一個對象的getValue(),那么B的返回值是什么?

    一次分析下先行發(fā)生原則中的各項規(guī)則:

    • A和B不在一個線程中,程序次序原則不適用;
    • 沒有同步塊,不會發(fā)生lock和unclock操作,管程鎖定規(guī)則不適用;
    • value變量沒有被volatile修飾,volatile變量規(guī)則不適用,
    • 線程啟動/終止/終端規(guī)則和對象終結(jié)規(guī)則不適用
    • 無適用的先行發(fā)生規(guī)則。

    所以可以斷定,即使A在操作時間上先行發(fā)生于B,但是無法確定B中的gertValue()方法的返回結(jié)果。即這里面的操作不是線程安全的。

    修復這個問題的方法:

    • 把getValue()和setValue()都設置為synchronized方法,可套用管程鎖定規(guī)則。
    • 把value定義為volatile,由于setValue()對value的修改不依賴原值,滿足volatile關(guān)鍵字使用場景,可套用volatile白能量規(guī)則來實現(xiàn)先行發(fā)生關(guān)系。

    由上面的例子可以得出一個結(jié)論:

    • 一個操作“時間上的先發(fā)生”不代表這個操作會是“先行發(fā)生”。
    • 一個操作若是“先行發(fā)生”也不代表這個操作是“時間上先發(fā)生”。典型的例子就是“指令重排”
    //以下操作在同一個線程中執(zhí)行 int i=1; int j=2;

    依據(jù)程序次序規(guī)則,“int i=1”的操作先行發(fā)生于“int j=2”。但是“int j=2”的代碼完全可能被處理器先執(zhí)行,這并不影響先行發(fā)生原則的正確性。因為我們無法再這條線程中感知到這一點。

    上面兩個例子,綜合起來可得到:

    時間先后順序和先行發(fā)生原則之間基本沒有太大的關(guān)系。

    所以我們衡量并發(fā)安全問題的時候不要受到時間順序的干擾,一切必須以現(xiàn)行發(fā)生原則為準。

    3. JVM參數(shù)中的Server模式和Client模式

    具體見《java并發(fā)編程實戰(zhàn)》P31

    對于服務器應用程序,無論是開發(fā)階段還是測試階段,當啟動JVM時一定都要指定-server命令行選項

    Server模式的JVM比client模式的JVM進行更多的優(yōu)化,例如將循環(huán)中未被修改的變量提升到循環(huán)外部,因此在開發(fā)模式(client模式的JVM)中能正確運行的代碼,可能會在部署環(huán)境(server模式的JVM)中運行失敗。如下代碼:
    ?

    volatile boolean asleep; while(!asleep){… }

    如果在代碼中忘記把asleep變量聲明為volatile變量,則Server模式的JVM會把asleep變量的判斷提升到循環(huán)體外部(這將導致一個無限循環(huán)),但Client模式的JVM不會這么做。

    在解決開發(fā)環(huán)境中出現(xiàn)無限循環(huán)問題時,解決這個問題的開銷遠小于解決在應用環(huán)境中出現(xiàn)無限循環(huán)的開銷

    整理自《深入理解Java虛擬機——JVM高級特性與最佳實踐》

    總結(jié)

    以上是生活随笔為你收集整理的Java内存模型、volatile、原子性、可见性、有序性、happens-before原则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。