Java Memory Model(Java記憶體模型),圍繞着在并發過程中如何處理可見性、原子性、有序性這三個特性而建立的模型。
JMM提供了volatile變量定義、final、synchronized塊來保證可見性。
例如:線程a在将共享變量x=1寫入主記憶體的時候,如何保證線程b讀取共享變量x的值為1,這就是JMM做的事情。JMM通過控制主記憶體與每個線程的本地記憶體之間的互動,來為java程式員提供記憶體可見性保證。
JMM提供保證了通路基本資料類型的原子性(其實在寫一個工作記憶體變量到主記憶體是分主要兩步:store、write),但是實際業務處理場景往往是需要更大的範圍的原子性保證,是以模型也提供了synchronized塊來保證。
這個概念是相對而言的,如果在本線程内,所有的操作都是有序的,如果在一個線程觀察另一個線程,所有的操作都是無序的,前句是“線程内表現為串行行為”,後句是“指令的重排序”和“工作記憶體和主記憶體同步延遲”現象,模型提供了volatile和synchronized來保證線程之間操作的有序性。
在執行程式時為了提高性能,編譯器和處理器常常會對指令做重排序(編譯器、處理器),就是因為這些重排序,是以可能會導緻多線程程式出現記憶體可見性問題(資料安全問題)和有序性問題。
JMM是如何處理的呢?
對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序
對于處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的記憶體屏障(memory barriers,intel稱之為memory fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序
總之一句話,JMM是通過禁止特定類型的編譯器重排序和處理器重排序來為程式員提供一緻的記憶體可見性保證。
A線程具體什麼時候重新整理共享資料到主記憶體是不确定的,假設我們使用了同步原語(synchronized,volatile和final),那麼重新整理的時間是确定的,例如:線程A釋放鎖後會同步到主記憶體,線程B擷取鎖後會同步主記憶體資料,即“A線程釋放鎖--B線程擷取鎖”可以實作A,B線程之間的通信。
The rules for happens-before are:
Program order rule. Each action in a thread happens-before every action in that thread that comes later in the program order.
Monitor lock rule. An unlock on a monitor lock happens-before every subsequent lock on that same monitor lock.
Volatile variable rule. A write to a volatile field happens-before every subsequent read of that same field.
Thread start rule. A call to Thread.start on a thread happens-before every action in the started thread.
Thread termination rule. Any action in a thread happens-before any other thread detects that thread has terminated, either by successfully return from Thread.join or by Thread.isAlive returning false.
Interruption rule. A thread calling interrupt on another thread happens-before the interrupted thread detects the interrupt (either by having InterruptedException tHRown, or invoking isInterrupted or interrupted).
Finalizer rule. The end of a constructor for an object happens-before the start of the finalizer for that object.
Transitivity. If A happens-before B, and B happens-before C, then A happens-before C.
----------------------------
happens-before就是“什麼什麼一定在什麼什麼之前運作”,也就是保證順序性。
因為CPU是可以不按我們寫代碼的順序執行記憶體的存取過程的,也就是指令會亂序或并行運作,
隻有上面的happens-before所規定的情況下,才保證順序性。
簡單介紹下。相對于記憶體,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可能還沒有進行寫入操作呢,雖然代碼順序上寫操作是在前面的。這就是可見性問題的由來。
并且,多個CPU之間的緩存也不保證明時同步,
也就是說你剛給一個變量指派,另一個線程立即擷取它的值,可能拿到的卻是舊值(或null),
因為兩個線程在不同的CPU執行,它們看到的緩存值不一樣,
隻有在synchronized或volatile或final的性況下才能保證正确性,
很多人用synchronized時隻記得有lock的功能,而忘記了線程間的可見性問題。
check()中的 n != n 好像永遠不會成立,因為他們指向同一個值,但非同步時卻很有可能發生。
另外,JMM不保證建立過程的原子性,讀寫并發時,可能看到不完整的對象,
這也是為什麼單例模式中著名的"雙重檢查成例"方法,在Java中行不通。(但.Net的記憶體模型保證這一點)
當然,在Java中單例的延遲加載可以用另一種方案實作(方案四):
目的是避開過多的同步,
但在Java中行不通,因為同步塊外面的if (instance == null)可能看到已存在,但不完整的執行個體。
JDK5.0以後版本若instance為volatile則可行
用volatile修飾的變量,線程在每次使用變量的時候,都會讀取變量修改後的最新的值。volatile很容易被誤用,用來進行原子性操作。