1、基礎與概念
(1)、共享性、互斥性、原子性、可見性、有序性。
http://www.cnblogs.com/paddix/p/5374810.html
(2)、JMM內存模型——描述線程本地內存和主內存之間的抽象關系。線程A和線程B之間通訊,需要通過主內存。
JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保證。
注意,線程本地內存只是一個抽象概念,它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器優化。
2、重排序
在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。
重排序分類
(1)、編譯器優化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
(2)、指令級并行的重排序:現代處理器采用了指令級并行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)、內存系統的重排序:由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。如下圖:
重排序的原則:as-if-serial語義
as-if-serial語義的意思是:不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序。數據依賴關系如下圖所示:
as-if-serial語義只能保證單線程下,重排序引起的問題。在多線程情況下,不存在數據依賴關系的重排序也會破壞程序的意圖。
單線程情況下,控制依賴關系的重排序,不影響最終結果。多線程情況下,則可能會破壞程序的意圖。
JMM禁止重排序的措施:
(1)、對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。
(2)、對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barriers,Intel稱之為MemoryFence)指令,通過內存屏障指令來禁止特定類型的處理器重排序。
從上圖可以看出:常見的處理器都允許Store-Load重排序;常見的處理器都不允許對存在數據依賴的操作做重排序。sparc-TSO和X86擁有相對較強的處理器內存模型,它們僅允許對寫-讀操作做重排序(因為它們都使用了寫緩沖區)。
內存屏障如上圖4種類型,StoreLoad Barriers是一個“全能型”的屏障,它同時具有其他3個屏障的效果。現代的多處理器大多支持該屏障(其他類型的屏障不一定被所有處理器支持)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩沖區中的數據全部刷新到內存中(Buffer Fully Flush)。
3、happen-before
JDK5之后,采用JSR-133版本的JMM內存模型,使用hap-pens-before的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。happens-before規則如下:
程序順序規則:一個線程中的每個操作,happens-before于該線程中的任意后續操作。?
監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。?
volatile變量規則:對一個volatile域的寫,happens-before于任意后續對這個volatile域的讀。?
傳遞性:如果A happens-before B,且B happens-beforeC,那么A happens-before C。
start()規則:如果線程A執行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
join()規則:如果線程A執行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
對于Java程序員來說,happens-before規則簡單易懂,它避免Java程序員為了理解JMM提供的內存可見性保證而去學習復雜的重排序規則以及這些規則的具體實現方法。
4、順序一致性
順序一致性內存模型是一個理論參考模型。順序一致性內存模型有兩大特性:
1)一個線程中的所有操作必須按照程序的順序來執行。
2)(不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
5、volatile
5-1、volatile實現原理
1)將當前處理器緩存行的數據寫回到系統內存。(Lock前綴指令.“緩存鎖定”,阻止兩個或以上處理器同時修改被緩存的內存區域)
2)這個寫回內存的操作會,其他處理器“嗅探”到,使在其他CPU里緩存了該內存地址的數據無效。
5-2、volatile特性
可見性:對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入。
原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種復合操作不具有原子性。
5-3、volatile寫-讀建立的happens-before關系
volatile的寫-讀與鎖的釋放-獲取有相同的內存效果。
這里A線程寫一個volatile變量后,B線程讀同一個volatile變量。A線程在寫volatile變量之前所有可見的共享變量,在B線程讀同一個volatile變量后,將立即變得對B線程可見。
5-4、volatile寫-讀的內存語義
volatile寫的內存語義如下:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀的內存語義如下:當讀一個volatile變量時,JMM會把該線程對應的本地內存置為無效。線程接下來將從主內存中讀取共享變量。
注:關于volatile變量重排序,嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具有相同的內存語義。
5-5、volatile和鎖的區別
由于volatile僅僅保證對單個volatile變量的讀/寫具有原子性,而鎖的互斥執行的特性可以確保對整個臨界區代碼的執行具有原子性。在功能上,鎖比volatile更強大;在可伸縮性和執行性能上,volatile更有優勢。
volatile的不能完全取代Synchronized的位置,只有在一些特殊的場景下,才能適用volatile。總的來說,必須同時滿足下面兩個條件才能保證在并發環境的線程安全:
(1)對變量的寫操作不依賴于當前值。如i++不符合
(2)該變量沒有包含在具有其他變量的不變式中。
6、鎖
6-1、鎖的獲取和釋放 建立的happens-before關系
6-2、鎖的釋放和獲取的內存語義
MM會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。
線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
線程A釋放鎖,隨后線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。
7、java concurrent包的通用化的實現模式
分析concurrent包的源代碼實現,會發現一個通用化的實現模式。
首先,聲明共享變量為volatile。
然后,使用CAS的原子條件更新來實現線程之間的同步。
同時,配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內存語義來實現線程之間的通信。
7、final
8、雙重檢查和延遲優化
上面代碼表面上看起來,似乎兩全其美:在多個線程試圖在同一時間創建對象時,會通過加鎖來保證只有一個線程能創建對象。在對象創建好之后,執行getInstance()將不需要獲取鎖,直接返回已創建好的對象。
雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!在線程執行到第4行代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。
問題的根源:
前面的雙重檢查鎖定示例代碼的第7行(instance = new Singleton();)創建一個對象。這一行代碼可以分解為如下的三行偽代碼:
memory = allocate();? //1:分配對象的內存空間
ctorInstance(memory);? //2:初始化對象
instance = memory;? ? //3:設置instance指向剛分配的內存地址
上面三行偽代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,詳情見參考文獻1的“Out-of-order writes”部分)。2和3之間重排序之后的執行時序如下:
memory = allocate();? //1:分配對象的內存空間
instance = memory;? ? //3:設置instance指向剛分配的內存地址
//注意,此時對象還沒有被初始化!
ctorInstance(memory);? //2:初始化對象
在知曉了問題發生的根源之后,我們可以想出兩個辦法來實現線程安全的延遲初始化。
1)不允許2和3重排序。 ? ? ? ? 2)允許2和3重排序,但不允許其他線程“看到”這個重排序。
當聲明對象的引用為volatile后,“問題的根源”的三行偽代碼中的2和3之間的重排序,在多線程環境中將會被禁止。(注意,這個解決方案需要JDK5或更高版本,因為從JDK5開始使用新的JSR-133內存模型規范,這個規范增強了volatile的語義。)
另外一種方式:通過內部類的加載來實現