Java內存模型

  • Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。
  • C/C++直接使用武力硬件和操做系統的內存模型,所以,有可能致使程序在一套平臺上併發徹底正常,而在另外一外一套平臺上併發訪問卻常常出錯,所以在某些場景就必須針對不一樣的平臺來編寫程序。
  • 定義Java內存模型,得足夠嚴謹,才能讓Java的併發內存訪問操做不會產生歧義;也得足夠寬鬆,使得虛擬機的實現有足夠的自由空間去利用硬件的各類特性(寄存器、高速緩存和指令集中某些特有的指令)來獲取更好的執行速度。

一、主內存與工做內存

  • Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。
  • 此處的變量(Variable)與Java編程中所說的變量有所區別,它包括了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,由於後者是線程私有的,不會被共享,天然就不會存在競爭問題。
  • 爲了得到較好的執行效能,Java內存模型並無限制執行引擎使用處理器的特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。
  • Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中(此處的主內存與介紹物理硬件時的主內存名字同樣,二者也能夠互相類比,但此處僅是虛擬機內存的一部分)。
  • 每條線程還有本身的工做內存(Working Memory,可與前面講的處理器高速緩存類比),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中的變量,而不能直接讀寫主內存中的變量。
  • 不一樣的線程之間也沒法直接訪問對工做內存中的變量,線程間變量值得傳遞均須要經過主內存來完成,線程、主內存、工做內存三者的交互關係如圖:
  • 這裏所講的主內存、工做內存與Java內存區域中的Java堆、棧、方法區等並非同一個層次的內存劃分,這二者基本上是沒有關係的,若是二者必定要勉強對應起來,那從變量、主內存、工做內存的定義來看,主內存主要對應於Java堆中的對象實例數據部分,而工做內存則對應於虛擬機棧中的部分區域。
  • 從更低層次上說,主內存就直接對應於物理硬件的內存,而爲了獲取更好的運行速度,虛擬機(甚至是硬件系統自己的優化措施)可能會讓工做內存優先存儲於寄存器和高速緩存中,由於程序運行時主要訪問讀寫的是工做內存。

二、內存間交互操做

  • 關於主內存與工做內存之間具體的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java內存模型中定義瞭如下8種操做來完成,虛擬機實現時必須保證下面說起的每個操做都是原子的、不可再分的(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許有例外)。
    • lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
    • unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
    • read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
    • load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
    • use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做。
    • assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
    • store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
    • write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。
  • 若是要把一個變量從主內存複製到工做內存,那就要順序地執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序地執行store和write操做。主要,Java內存模型只要上述兩個操做必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其餘指令的,如對主內存的變量a、b進行訪問時,一種可能出現順序是read a、read b、load b、load a。
  • 除此以外,Java內存模型還規定了在執行上述8種基本操做時必須知足以下規則:
    • 不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。
    • 不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。
    • 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存中。
    • 一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操做以前,必須先執行了assign和load操做。
    • 一個變量在同一個時刻只容許一個線程對其進行lock操做,但lock操做能夠被同一條線程反覆執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。
    • 若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
    • 若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定住的變量。
    • 對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write)。
  • 這8種內存訪問操做以及上述規則限定,再加上稍後介紹的對volatile的一些特殊規定,就已經徹底肯定了Java程序中哪些內存訪問操做在併發下是安全。

三、對於volatile型變量的特殊規則

  • 關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制,可是它並不容易徹底被正確、完整地理解,遇到須要處理多線程數據競爭問題的時候一概使用synchronized來進行同步。
  • Java內存模型對volatile專門定義了一些特殊的訪問規則。
  • 當一個變量定義爲volatile以後,它將具有兩種特性,第一是保證此變量對全部線程的可見性,這裏的「可見性」是指當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量不能作到這一點,普通變量的值在線程間傳遞均須要經過主內存來完成,例如,線程A修改一個普通變量的值,而後向主內存進行回寫,另一個線程B在線程A回寫完成了以後再從主內存進行讀取操做,新變量值纔會對線程B可見。
  • 關於volatile變量的可見性,常常會被開發人員誤解,認爲如下描述成立:「volatile變量對全部線程是當即可見的,對volatile變量全部的寫操做都能馬上反應到其餘線程之中,換句話說,volatile變量在各個線程中是一致的,全部基於volatile變量的運算在併發下是安全的」。這句話的論據部分並無錯,可是其論據並不能得出「基於volatile變量的運算在併發下是安全的」這個結論。
  • volatile變量在各個線程的工做內存中不存在一致性問題(在各個線程的工做內存中,volatile變量也能夠存在不一致的狀況,但因爲每次使用以前都要先刷新,執行引擎看不到不一致的狀況,所以能夠認爲不存在一致性問題),可是Java裏面的運算並不是原子性操做,致使volatile變量的運算在併發下同樣是不安全的。
public class VolatileTest {
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }
    private static final int THREAD_COUNT = 20;
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREAD_COUNT];
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        // 等待全部累加線程都結束
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(race);
    }
}
  • 若是正確的話,最後的輸出結果是200000。實際上每次都是一個小於200000的數字。
  • 問題出在自增運算「race++」之中,咱們用javap反編譯這段代碼後會獲得以下代碼:
public static void increace();
Code:
    Stack=2, Locals=0, Args_size=0
    0:    getstatic    #13; // Field race:1
    3:    iconst_1
    4:    iadd
    5:    putstatic    #13; // Field race:1
    8:    return 
LineNumberTable:
    line 14: 0
    line 15: 8
  • 發現只有一行代碼的increase()方法在Class文件中是由4條字節碼指令構成的(return指令不是由race++產生的,這條指令能夠不計算),從字節碼層面上很容易就分析出併發失敗的緣由:當getstatic指令把race的值取到操做棧頂時,volatile關鍵字保證了race的值在此時是正確的,可是在執行iconst_一、iadd這些指令的時候,其餘線程可能已經把race的值加大了,而在操做棧頂的值就變成了過時的數據,因此putstatic指令執行後就能夠把較小的race值同步回主內存之中。
  • 客觀地說,筆者在此使用字節碼來分析併發問題,仍然是不嚴謹的,由於即便編譯出來只有一條字節碼指令,也並不意味執行這條指令就是一個原子操做。一條字節碼指令在解釋執行時,解釋器將要運行許多代碼才能實現它的語義。
  • 若是是編譯執行,一條字節碼指令也可能轉化成若干條本地機器指令,此處使用-XX:+PrintAssembly參數輸出反編譯來分析會更加嚴謹。
  • 因爲volatile變量只能保證可見性,在不符合如下兩條規則的運算場景中,咱們仍然要經過加鎖(使用synchronized或java.util.concurrent中的原子類)來保證原子性。
    • 運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值。
    • 變量不須要與其餘的狀態變量共同參與不變約束。
  • 像下面代碼的這類場景就很適合使用volatile變量來控制併發,當shutdown()方法被調用時,能保證全部線程中執行的doWork()方法都當即停下來。
volatile boolean shutdownRequested;

public void shutdown() {
    shutdownRequested = true;
}

public void  doWork() {
    while (!shutdownRequested) {
        // do stuff
    }
}
  • 使用volatile變量的第二個語義是禁止指令重排序優化,普通的變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。
  • 由於在一個線程的方法執行過程當中沒法感知到這點,這樣就是Java內存模型中描述的所謂的「線程內變現爲串行的語義」(Within-Thread As-If-Serial Semantics)。
  • 經過一個例子來看看爲什麼指令重排序會干擾程序的併發執行:
Map configOptions;
char[] configText;
// 此變量必須定義爲volatile
volatile boolean initialized = false;

// 假設如下代碼在線程A中執行
// 模擬讀取配置信息,當讀取完成後將initialized設置爲true以通知其餘線程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

// 假設如下代碼在線程B中執行
// 等待initialized爲true,表明線程A已經把配置信息初始化完成
while (!initialized) {
    sleep();
}
// 使用線程A中初始化好的配置信息
doSomethingWithConfig();
  • 若是定義initialized變量時沒有使用volatile修飾,就可能會因爲指令重排序的優化,致使位於線程A中最後一句的代碼「initialized = true」被提早執行(這裏雖然使用Java做爲僞代碼,但所指的重排序優化是機器級的優化操做,提早執行是指這句話對應的彙編代碼被提早執行),這樣在線程B中使用配置信息的代碼就可能出現錯誤,而volatile關鍵字則能夠避免此類狀況的發生。
  • 一個能夠實際操做運行的例子來分析voaltile關鍵字是如何禁止指令重排序優化的。下面代碼是一段標準的DCL單例代碼,能夠觀察加入volatile和未加入volatile關鍵字時所生產彙編代碼的差異。
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變量賦值部分以下:
0f: mov $0x3375cdb0, %esi
14: mov $eax, 0x150(%esi)
1a: shr $0x9, %esi
1d: movb $0x0, 0x1104800(%esi)
24: lock addl $0x0, (%esp)

Singleton::getInstance@24
  • volatile屏蔽指令重排序的語義在JDK1.5中才徹底修復,這點也是在JDK1.5以前的Java中沒法徹底地使用DCL(雙鎖檢測)來實現單例模式的緣由。
  • 經過對比就會發現,關鍵變化在於有volatile修飾的變量,賦值後(前面mov %eax, 0x150(%esi)這句即是賦值操做)多執行了一個「lock add1 $0x0,(%esp)」操做,這個操做至關於一個內存屏障(Memory Barrier或Memory Fence,指重排序時不能把後面的指令重排序到內存屏蔽以前的位置),只有一個CPU訪問內存時,並不須要內存屏障;
  • 但若是有兩個或更多CPU訪問同一塊內存,且其中有一個在觀測另外一個,就須要內存屏障來保證一致性了。
  • 這句指令中的「add1 $0x0, (%esp)」(把ESP寄存器的值加0)顯然是一個空操做(採用這個空操做而不是空操做指令nop是由於IA32手冊規定lock前綴不容許配合nop指令使用),關鍵在於lock前綴,查詢IA32手冊,它的做用是使得本CPU的Cache寫入了內存,該寫入動做也會引發別的CPU或者別的內核無效化(Invalidate)其Cache,這種操做至關於對Cache中的變量作了一次前面介紹Java內存模式中所說的「store和write」操做。因此經過這樣一個空操做,可以讓前面volatile變量的修改對其餘CPU當即可見。
  • 那爲什麼說它禁止指令重排序呢?從硬件架構上講,指令重排序是指CPU採用了容許將多條指令不按程序規定的順序分發送給各相應電路單元處理。但並非說指令任意重排,CPU須要能正確處理指令依賴狀況以保障程序能得出正確的執行結果。
  • 譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值減去3,這時指令1和指令2是有依賴的,它們之間的順序不能重排——(A+10)* 2 與 A * 2 + 10顯然不相等,但指令3能夠重排到指令一、2以前或者中間,只要保證CPU執行後面依賴到A、B值的操做時能獲取到正確的A和B便可。
  • 因此在本內CPU中,重排序看起來依然是有序的。所以,lock add1 $0x0,(%esp)指令把修改同步到內存時,意味着全部以前的操做都已經執行完成,這樣便造成了「指令重排序沒法越過內存屏障」的效果。
  • 解決了volatile的語義問題,再來看看在衆多保障併發安全的工具中選用volatile的意義——它能讓咱們的代碼比使用其餘的同步工具更快嗎?
  • 在某些狀況下,volatile的同步機制的性能確實要優於鎖(使用synchronized關鍵字或java.util.concurrent包裏面的鎖),可是因爲虛擬機對鎖實行的許多消除和優化,使得咱們很難量化地認爲volatile就會比synchronized快多少。
  • 若是讓volatile本身與本身比較,那能夠肯定一個原則:volatile變量讀操做的性能消耗與普通變量幾乎沒有什麼差異,可是寫操做則可能會慢一些,由於它須要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。
  • 大多數場景下volatile的總開銷仍然要比鎖低,咱們在volatile與鎖之中選擇的惟一依據僅僅是volatile的語義可否知足使用場景的需求。
Java內存模型中對volatile變量定義的特殊規則。
  • 假定T表示一個線程,V和W分別表示兩個volatile型變量,那麼在進行read、load、use、assign、store和write操做時須要知足以下規則:
    • 只有當線程T對變量V執行的前一個動做是load的時候,線程T才能對變量V執行use動做:而且,只有當線程T對變量V執行的後一個動做是use的時候,線程T才能對變量V執行load動做。線程T對變量V的use動做能夠認爲是和線程T對變量V的load、read動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其餘線程對變量V所作的修改後的值)。
    • 只有當線程T對變量V執行的前一個動做是assign的時候,線程T才能對變量V執行store動做;而且,只有當線程T對變量V執行後一個動做是store的時候,線程T才能對變量V執行assign動做。線程T對變量V的assign動做能夠認爲是和線程T對變量V的store、write動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次修改V後都必須馬上同步回主內存中,用於保證其餘線程能夠看到本身對變量V所作的修改)。
    • 假定動做A是線程T對變量V實施的use或assign動做,假定動做F是和動做A相關聯的load或者store動做,假定動做P是和動做F相應的對應量V的read或write動做;相似的,假定動做B是線程T對變量W實施的use或assign動做,假定動做G是和動做B相關聯的load或store動做,假定動做Q是和動做G相應的對變量W的read或write動做。若是A先於B,那麼P先於Q(這條規則要求volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同)。

四、對於long和double型變量的特殊規則

  • Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操做都具備原子性,可是對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現選擇能夠不保證64數據類型的load、store、read和write這4個操做的原子性,這點就是所謂的long和double的非原子協議。
  • 若是有多個線程共享一個並未聲明爲volatile的long或double類型的變量,而且同時對它們進行讀取和修改操做,那麼某些線程可能會讀取到一個即非原值,也不是其餘線程修改值的表明了「半個變量」的數值。
  • 不過這種讀取到「半個變量」的狀況很是罕見(在目前商用Java虛擬機中不會出現),由於Java內存模型雖然容許虛擬機不把long和double變量的讀寫實現成原子操做,但容許虛擬機選擇把這些操做實現爲具備原子性的操做,並且還「強烈建議」虛擬機這樣實現。
  • 在實際開發中,目前各類平臺下的商用虛擬機幾乎都選擇把64位數據的讀寫操做做爲原子操做來對待,所以咱們在編寫代碼時通常不須要把用到的long和double變量專門聲明爲volatile。

五、原子性、可見性與有序性

原子性(Atomicity)
  • 由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定)。
  • 若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供了lock和unlock操做來知足這種需求,儘管虛擬機未把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊——synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
可見性(Visibility)
  • 可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值這種依賴主內存做爲傳遞媒介的方式來實現可見性的,不管是普通變量仍是volatile變量都是如此,普通變量與volatile變量的區別是,volatile的特殊規則保證了新值能當即同步到主內存,以及每次使用前當即從主內存刷新。所以,能夠說volatile保證了多線程操做時變量的可見性,而普通變量則不能保證這一點。
  • 除了volatile以外,Java還有兩個關鍵字能實現可見性,即synchronized和final。同步塊的可見性是由「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)」這條規則得到的;
  • 而final關鍵字的可見性是指:被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把「this」的引用傳遞出去(this引用逃逸是一件很危險的事情,其餘線程有可能經過這個引用訪問到「初始化了一半」的對象),那在其餘線程中就能看見final字段的值。以下代碼,變量i和j都具有可見性,它們無須同步就能被其餘線程正確訪問。
public static final int i;
public final int j;
static {
    i = 0;
    // do something
}
{
    // 也能夠選擇在構造函數中初始化
    j = 0;
    // do something
}
有序性(Ordering)
  • 若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。
  • 前半句是指「線程內表現爲串行的語義」(Within-Thread As-If-Serial Semantics),後半句是指「指令重排序」現象和「工做內存與主內存同步延遲」現象。
  • Java語義提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由「一個變量在同一個時刻只容許一條線程對其進行lock操做」這條規則得到的,這條規則決定了持有同一個鎖的兩個同步塊只能串行地進入。

六、先行發生原則

  • 先行發生是Java內存模型中定義的兩項操做之間的偏序關係,若是說操做A先發生於操做B,其實就是說在發生操做B以前,操做A產生的影響能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了信息、調用了方法等。
// 如下操做在線程A中執行
i = 1;
// 如下操做在線程B中執行
j = i;
// 如下操做在線程C中執行
i = 2;
  • 假設線程A中的操做「i=1」先行發生於線程B的操做「j=i」,那麼能夠肯定在線程B的操做以後,變量j的值必定等於1,得出這個結論的依據有兩個:一是根據先行發生原則,「i = 1」的結果能夠被觀察到;二是線程C還沒「登場」,線程A操做結束以後沒有其餘線程會修改變量i的值。
  • 如今再來考慮線程C,咱們依然保持線程A和線程B之間的先行發生關係,而線程C出如今線程A和線程B的操做之間,可是線程C與線程B沒有先行發生關係,那j的值會是多少呢?答案是不肯定!1和2都有可能,由於線程C對變量i的影響可能會被線程B觀察到,也可能不會,這時候線程B就存在讀取到過時數據的風險,不具有多線程安全性。
  • 下面是Java內存模型下一些「自然的」先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,他們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。
    • 程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。
    • 管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是同一個鎖,而「後面」的是指時間上的前後順序。
    • volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。
    • 線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做。
    • 線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
    • 線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。
    • 對象終結規則:一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
    • 傳遞性:若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
  • Java語言無須任何同步手段保障就能成立的先行發生規則就只有上面這些了,如何使用這些規則去斷定操做間是否具有順序性,對於讀寫共享變量的操做來講,就是線程是否安全。
  • 感覺一下「時間上的前後順序」與「先行發生」之間有什麼不一樣。
private int value = 0;

public void setVlaue(int value) {
    this.value = value;
}

public int getValue() {
    return value;
}
  • 代碼顯示的是一組普通的getter/setter方法,假設存在線程A和B,線程A先(時間上的前後)調用了「setValue(1)」,而後線程B調用了同一個對象的「getValue」,那麼線程B收到的返回值是什麼?
  • 咱們依次分析一下先行發生原則中的各項規則,因爲兩個方法分別由線程A和線程B調用,不在一個線程中,因此程序次序規則在這裏不適用;因爲沒有同步塊,天然就不會發生lock和unlock操做,因此管程規定規則不適用;
  • 因爲沒有同步塊,天然就不會發生lock和unlock操做,因此管程鎖定規則不適用;
  • 因爲value變量沒有被volatile關鍵字修飾,因此volatile變量規則不適用;
  • 後面的線程啓動、終止、中斷規則和對象終結規則也和這裏徹底沒有關係。
  • 由於沒有一個使用的先行發生規則,因此最後一條傳遞性也不會適用。
  • 所以咱們能夠斷定儘管線程A在操做時間上先於線程B,可是沒法肯定線程B中「getValue()」方法返回結果,這操做不是線程安全的。
  • 咱們至少有兩種比較簡單的方案能夠選擇:要麼把getter/setter方法都定義爲synchronized方法,這樣就能夠套用管程鎖定規則:要麼把value定義爲volatile變量,因爲setter方法對value的修改不依賴value的原值,知足volatile關鍵字使用場景,這樣就能夠套用volatile變量規則來實現先行發生關係。
  • 經過上面的例子,咱們能夠得出結論:一個操做「時間上的先發生」不表明這個操做會是「先行發生」,那若是一個操做「先行發生」是否就能推導出這個操做一定是「時間上的先發生」呢?很遺憾,這個推論是不成立的,一個典型的例子就是屢次提到的「指令重排序」。
// 如下操做在同一個線程中執行
int i = 1;
int j = 2;
  • 兩條賦值語句在同一個線程之中,根據程序次序規則,「int i = 1」的操做先行發生於"int j = 2",可是「int j = 2」的代碼徹底可能先被處理器執行,這並不影響先行發生原則的正確性,由於咱們在這條線程之中沒有辦法感知這點。
  • 證實了一個結論:時間前後順序與先行發生原則之間基本沒有太大的關係,因此咱們衡量併發安全問題的時候不要受到時間順序的干擾,一切必須以先行發生原則爲準。
相關文章
相關標籤/搜索