Java happen-before規則緩存
synchronized、大部分鎖,衆所周知的一個功能就是使多個線程互斥/串行的(共享鎖容許多個線程同時訪問,如讀鎖)訪問臨界區,但他們的第二個功能 —— 保證變量的可見性 —— 常被遺忘。安全
爲何存在可見性問題?簡單介紹下。相對於內存,CPU的速度是極高的,若是CPU須要存取數據時都直接與內存打交道,在存取過程當中,CPU將一直空閒,這是一種極大的浪費,媽媽說,浪費是很差的,因此,現代的CPU裏都有不少寄存器,多級cache,他們比內存的存取速度高多了。某個線程執行時,內存中的一份數據,會存在於該線程的工做存儲中(working memory,是cache和寄存器的一個抽象,這個解釋源於《Concurrent Programming in Java: Design Principles and Patterns, Second Edition》§2.2.7,原文:Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. 有很多人以爲working memory是內存的某個部分,這多是有些譯做將working memory譯爲工做內存的緣故,爲避免混淆,這裏稱其爲工做存儲,每一個線程都有本身的工做存儲),並在某個特定時候回寫到內存。單線程時,這沒有問題,若是是多線程要同時訪問同一個變量呢?內存中一個變量會存在於多個工做存儲中,線程1修改了變量a的值何時對線程2可見?此外,編譯器或運行時爲了效率能夠在容許的時候對指令進行重排序,重排序後的執行順序就與代碼不一致了,這樣線程2讀取某個變量的時候線程1可能尚未進行寫入操做呢,雖然代碼順序上寫操做是在前面的。這就是可見性問題的由來。多線程
咱們沒法枚舉全部的場景來規定某個線程修改的變量什麼時候對另外一個線程可見。但能夠制定一些通用的規則,這就是happens-before。app
「先行發生」(happen—before)的規則,它是Java內存模型中定義的兩項操做之間的偏序關係,若是操做A先行發生於操做B,其意思就是說,在發生操做B以前,操做A產生的影響都能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等,它與時間上的前後發生基本沒有太大關係。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。函數
舉例來講,假設存在以下三個線程,分別執行對應的操做:性能
---------------------------------------------------------------------------this
線程A中執行以下操做:i=1編碼
線程B中執行以下操做:j=ispa
線程C中執行以下操做:i=2.net
---------------------------------------------------------------------------
假設線程A中的操做"i=1" happen—before線程B中的操做"j=i",那麼就能夠保證在線程B的操做執行後,變量j的值必定爲1,即線程B觀察到了線程A中操做"i=1"所產生的影響;如今,咱們依然保持線程A和線程B之間的happen—before關係,同時線程C出如今了線程A和線程B的操做之間,可是C與B並無happen—before關係,那麼j的值就不肯定了,線程C對變量i的影響可能會被線程B觀察到,也可能不會,這時線程B就存在讀取到不是最新數據的風險,不具有線程安全性。
下面是Java內存模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們進行隨機地重排序。
一、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做。
二、管理鎖定規則:一個unlock操做happen—before後面(時間上的前後順序,下同)對同一個鎖的lock操做。
三、volatile變量規則:對一個volatile變量的寫操做happen—before後面對該變量的讀操做。
四、線程啓動規則:Thread對象的start()方法happen—before此線程的每個動做。
五、線程終止規則:線程的全部操做都happen—before對此線程的終止檢測,能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
六、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。
七、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
八、傳遞性:若是操做A happen—before操做B,操做B happen—before操做C,那麼能夠得出A happen—before操做C。
」時間上執行的前後順序「與」happen—before「之間有何不一樣呢?
一、首先來看操做A在時間上先與操做B發生,是否意味着操做A happen—before操做B?
一個經常使用來分析的例子以下:
private int value = 0; public int get(){ return value; } public void set(int value){ this.value = value; }
假設存在線程A和線程B,線程A先(時間上的先)調用了setValue(3)操做,而後(時間上的後)線程B調用了同一對象的getValue()方法,那麼線程B獲得的返回值必定是3嗎?
對照以上八條happen—before規則,發現沒有一條規則適合於這裏的value變量,從而咱們能夠斷定線程A中的setValue(3)操做與線程B中的getValue()操做不存在happen—before關係。所以,儘管線程A的setValue(3)在操做時間上先於操做B的getvalue(),但沒法保證線程B的getValue()操做必定觀察到了線程A的setValue(3)操做所產生的結果,也便是getValue()的返回值不必定爲3(有多是以前setValue所設置的值)。這裏的操做不是線程安全的。
所以,「一個操做時間上先發生於另外一個操做」並不表明「一個操做happen—before另外一個操做」。
解決方法:能夠將setValue(int)方法和getValue()方法均定義爲synchronized方法,也能夠把value定義爲volatile變量(value的修改並不依賴value的原值,符合volatile的使用場景),分別對應happen—before規則的第2和第3條。注意,只將setValue(int)方法和getvalue()方法中的一個定義爲synchronized方法是不行的,必須對同一個變量的全部讀寫同步,才能保證不讀取到陳舊的數據,僅僅同步讀或寫是不夠的 。
二、其次來看,操做A happen—before操做B,是否意味着操做A在時間上先與操做B發生?
x = 1; y = 2;
假設同一個線程執行上面兩個操做:操做A:x=1和操做B:y=2。根據happen—before規則的第1條,操做A happen—before 操做B,可是因爲編譯器的指令重排序(Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。這個過程經過叫作指令的重排序。指令重排序存在的意義在於:JVM可以根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的從新排序機器指令,使機器指令更符合CPU的執行特色,最大限度的發揮機器的性能。在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整)等緣由,操做A在時間上有可能後於操做B被處理器執行,但這並不影響happen—before原則的正確性。
所以,「一個操做happen—before另外一個操做」並不表明「一個操做時間上先發生於另外一個操做」。
最後,一個操做和另外一個操做一定存在某個順序,要麼一個操做或者是先於或者是後於另外一個操做,或者與兩個操做同時發生。同時發生是徹底可能存在的,特別是在多CPU的狀況下。而兩個操做之間卻可能沒有happen-before關係,也就是說有可能發生這樣的狀況,操做A不happen-before操做B,操做B也不happen-before操做A,用數學上的術語happen-before關係是個偏序關係。兩個存在happen-before關係的操做不可能同時發生,一個操做A happen-before操做B,它們一定在時間上是徹底錯開的,這實際上也是同步的語義之一(獨佔訪問)。
參考:http://ifeve.com/easy-happens-before/
http://blog.csdn.net/ns_code/article/details/17348313
=============END=============