在併發編程中,咱們一般要處理兩個問題:線程以前如何通訊與線程之間如何同步。java
通訊是指線程之間如何交換信息,一般的通訊手段有:共享內存與消息傳遞(語言不一樣,通訊機制不一樣,java使用的是共享內存的併發模型)程序員
在共享內存的併發模型中,線程之間共享信息的公共狀態,經過對信息公共狀態的讀 - 寫來隱使地進行線程通訊;而在消息傳遞的併發模型中,因爲線程直接沒有信息的公共狀態,因此只能傳遞明確消息來顯式地進行通訊編程
同步是指控制不一樣線程的操做發生相對順序的機制,在共享內存併發模型裏,同步是顯式進行的,程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。數組
因爲java使用的是共享內存的併發模型,線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明,若是不理解隱式進行的線程之間通訊的工做機制,極可能會遇到各類奇怪的內存可見性問題。緩存
上面已經給出來了Java使用的是共享內存的併發模型,在前一篇JVM的內存結構中,只有兩塊內存是被線程之間共享的,堆和方法區,因此這兩塊區域也是做爲共享內存的信息存儲區,包括實例域、靜態域和數組元素(本文使用「共享變量」這個術語代指實例域,靜態域和數組元素)bash
Java 線程之間的通訊由 Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(localmemory),本地內存中存儲了該線程以讀 / 寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。Java 內存模型的抽象示意圖以下:多線程
線程 A 與線程 B 之間如要通訊的話,必需要經歷下面 2 個步驟(假設共享變量爲X):1.線程 A 先更新本地內存X,而後把更新過的共享變量刷到主內存中去。併發
2.線程 B 到主內存中去讀取線程 A 已更新過的共享變量X,同時更新本地內存中的共享變量(若已存在該共享變量)。性能
這個通訊過程必需要通過主內存。JMM 經過控制主內存與每一個線程的本地內存之間的交互,來爲 java 程序員提供內存可見性保證。優化
在執行程序時爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分三種類型:
1.編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。
2.指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
3.內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
從 java 源代碼到最終實際執行的指令序列,會分別經歷下面三種重排序:
上述的 1 屬於編譯器重排序,2和3屬於處理器重排序。這些重排序均可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是全部的編譯器重排序都要禁止)。對於處理器重排序,JMM 的處理器重排序規則會要求 java 編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel 稱之爲 memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。
說了這麼多,舉個例子:
int a = 1; // step 1
boolean b = true; // step 2
複製代碼
正常狀況下,step2不依賴step1的執行,因此step1跟step2會發生重排序。可是若是後面有這麼一段代碼:
if (b) {
int c = a; //step 3
System.out.println(c);
}
複製代碼
假設一種狀況,在編譯器重排序下,線程1中,若是step2在step1以前執行,線程2走到了step3,而且step1尚未把a的值1刷新到主內存,此時C的值並不會是1,這就是編譯器重排序引來的問題,前面也講處處理器重排序插入特定類型的內存屏障,這就是來解決編譯器重排序帶來的問題。
現代的處理器使用寫緩衝區來臨時保存向內存寫入的數據。寫緩衝區能夠保證指令流水線持續運行,它能夠避免因爲處理器停頓下來等待向內存寫入數據而產生的延遲。可是對內存的讀/寫操做的執行順序,不必定與內存實際發生的讀 / 寫操做順序一致!
爲了保證內存可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。JMM 把內存屏障指令分爲下列四類:
StoreLoad Barriers 是一個「全能型」的屏障,它同時具備其餘三個屏障的效果。現代的多處理器大都支持該屏障(其餘類型的屏障不必定被全部處理器支持)。執行該屏障開銷會很昂貴,由於當前處理器一般要把寫緩衝區中的數據所有刷新到內存中(buffer fully flush)。 同時這個屏障也解決了上面的問題,給c賦值前必需要先去a,那麼在取a以前插入LoadStore,就能夠保證讀取到最新的a值了!