Java併發編程-volatile

一直覺得多線程環境的同步只能經過這個來實現的,事實上Java還提供了另一個更加輕量級的實現-volatile,若是說synchronized實現了數據在同一時刻只能有一個線程對數據訪問的話,那麼volatile實現的就是同時能夠多個線程在訪問數據,可是隻要數據發生了變化,便確保其餘線程及時「感知」這種變化。html

一、CPU、主存及高速緩存的概念java

  計算機的硬件組成能夠抽象爲由總線、IO設備、主存、處理器(CPU)等組成。其中數據存放在主存中,CPU負責指令的執行,CPU的指令執行很是快,大部分簡單指令的執行只須要一個時鐘週期,而一次主內存數據的讀取則須要幾十到幾百個時鐘週期,那麼CPU從主存中讀寫數據就會有很大的延遲。這個時候就產生了高速緩存的概念。linux

  也就是說,當程序在運行過程當中,會將運算須要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就能夠直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束以後,再將高速緩存中的數據回寫到主存當中,經過這種方式來下降CPU從主存中獲取數據的延遲。大體的示意圖以下:git

圖一這個模型,能夠簡單的認爲是單核模型,在這個模型裏面,以i++這個操做爲例,程序執行時,會先從主內存中獲取i的值,複製到高速緩存,而後CPU從高速緩存中加載並執行+1操做,操做完成後回寫到高速緩存,最後再從高速緩存回寫到主內存。單核模型這樣操做沒有任何問題,可是計算機自產生以來,一直追求的兩個目標,一個是如何作的更多,另外一個就是如何計算得更快,這樣帶來的變化就是單核變成多核,高速緩存分級存儲。大體的示意圖以下:github

在圖二示意圖裏面,i++這個操做就有問題了,由於多核CPU能夠線程並行計算,在Core 0和Core 1中能夠同時將i複製到各自緩存中,而後CPU各自進行計算,假設初始i爲1,那麼預期咱們但願是2,可是實際因爲兩個CPU各自前後計算後最終主內存中的i多是2,也多是其餘值。編程

  這個就是硬件內存架構中存在的一個問題,緩存一致性問題,就是說核1改變了變量i的值以後,核0是不知道的,存放的仍是舊值,最終對這樣的一個髒數據進行操做。緩存

  爲此,CPU的廠商定製了相關的規則來解決這樣一個硬件問題,主要有以下方式:多線程

  1)  總線加鎖,其實很好理解總線鎖,我們來看圖二,前面提到了變量會從主內存複製到高速緩存,計算完成後,會再回寫到主內存,而高速緩存和主內存的交互是會通過總線的。既然變量在同一時刻不能被多個CPU同時操做,會帶來髒數據,那麼只要在總線上阻塞其餘CPU,確保同一時刻只能有一個CPU對變量進行操做,後續的CPU讀寫操做就不會有髒數據。總線鎖的缺點也很明顯,有點相似將多核操做變成單核操做,因此效率低;架構

  2)  緩存鎖,即緩存一致性協議,主要有MSI、MESI、MOSI等,這些協議的主要核心思想:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。併發

  二、Java內存模型

  在Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽各個硬件平臺和操做系統的內存訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。在此以前,主流程序語言(C/C++等)直接使用物理硬件和操做系統的內存模型(能夠理解爲相似於直接使用了硬件標準),都或多或少的在不一樣的平臺有着不同的執行結果。 

  Java內存模型的主要目標是定義程序中各個變量的訪問規則,即變量在內存中的存儲和從內存中取出變量這樣的底層細節。其規定了全部變量都存儲在主內存,每一個線程還有本身的工做內存,線程讀寫變量時需先複製到工做內存,執行完計算操做後再回寫到主內存,每一個線程還不能訪問其餘線程的工做內存。大體示意圖以下:

 

 

圖三咱們能夠理解爲和圖二表達的是一個意思,工做內存能夠當作是CPU高速緩存、寄存器的抽象,主內存能夠當作就是物理硬件中主內存的抽象,圖二這個模型會存在緩存一致性問題,圖三一樣也會存在緩存一致性問題。

  另外,爲了得到較好的執行性能,Java內存模型並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在Java內存模型中,還會存在指令重排序的問題。

  Java語言又是怎麼來解決這兩個問題的呢?就是經過volatile這個關鍵字來解決緩存一致性和指令重排問題,volatile做用就是確保可見性和禁止指令重排。

3、volatile背後實現

  那麼volatile又是怎樣來確保的可見性和禁止指令重排呢?我們先來寫一段單例模式代碼來看看。

 1 public class Singleton {
 2     private static volatile  Singleton instance;
 3 
 4     public static Singleton getInstance() {
 5         if (instance == null) {
 6             synchronized (Singleton.class) {
 7                 if (instance == null) {
 8                     instance = new Singleton();
 9                 }
10             }
11         }
12         return instance;
13     }
14 
15     public static void main(String[] args) {
16         Singleton.getInstance();
17     }
18 }

先看看字節碼層面,JVM都作了什麼。

圖四

從圖四能夠看出,沒有什麼特別之處。既然在字節碼層面咱們看不出什麼端倪,那下面就看看將代碼轉換爲彙編指令能看出什麼端倪。轉換爲彙編指令,能夠經過-XX:+PrintAssembly來實現,window環境具體如何操做請參考此處(https://dropzone.nfshost.com/hsdis.xht)。不過比較惋惜的是我雖然編譯成功了hsdis-i386.dll(圖五),放置在了JDK8下的多個bin目錄,一致在報找不到這個dll文件因此我決定換個思路一窺究竟。

        圖五

這個思路就是去閱讀openJDK的源代碼。其實經過javap能夠看到volatile字節碼層面有個關鍵字ACC_VOLATILE,經過這個關鍵字定位到accessFlags.hpp文件,代碼以下:

bool is_volatile    () const         { return (_flags & JVM_ACC_VOLATILE    ) != 0; }

再搜索關鍵字is_volatile,在bytecodeInterpreter.cpp能夠看到以下代碼:

 1 //
 2           // Now store the result
 3           //
 4           int field_offset = cache->f2_as_index();
 5           if (cache->is_volatile()) {
 6             if (tos_type == itos) {
 7               obj->release_int_field_put(field_offset, STACK_INT(-1));
 8             } else if (tos_type == atos) {
 9               VERIFY_OOP(STACK_OBJECT(-1));
10               obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
11               OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
12             } else if (tos_type == btos) {
13               obj->release_byte_field_put(field_offset, STACK_INT(-1));
14             } else if (tos_type == ltos) {
15               obj->release_long_field_put(field_offset, STACK_LONG(-1));
16             } else if (tos_type == ctos) {
17               obj->release_char_field_put(field_offset, STACK_INT(-1));
18             } else if (tos_type == stos) {
19               obj->release_short_field_put(field_offset, STACK_INT(-1));
20             } else if (tos_type == ftos) {
21               obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
22             } else {
23               obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
24             }
25             OrderAccess::storeload();
26           }

在這段代碼中,會先判斷tos_type,後面分別有不一樣的基礎類型的實現,好比int就調用release_int_field_put,byte就調用release_byte_field_put等等。以int類型爲例,繼續搜索方法release_int_field_put,在oop.hpp能夠看到以下代碼:

void release_int_field_put(int offset, jint contents);

這段代碼實際是內聯oop.inline.hpp,具體的實現是這樣的:

inline void oopDesc::release_int_field_put(int offset, jint contents)       { OrderAccess::release_store(int_field_addr(offset), contents);  }

其實看到這,能夠看到上一篇文章很熟悉的oop.hpp和oop.inline.hpp,就是很熟悉的Java對象模型。繼續看OrderAccess::release_store,能夠在orderAccess.hpp找到對應的實現方法:

static void     release_store(volatile jint*    p, jint    v);

實際上這個方法的實現又有不少內聯的針對不一樣的CPU有不一樣的實現的,在src/os_cpu目錄下能夠看到不一樣的實現,以orderAccess_linux_x86.inline.hpp爲例,是這麼實現的:

inline void     OrderAccess::release_store(volatile jint*    p, jint    v) { *p = v; }

能夠看到其實Java的volatile操做,在JVM實現層面第一步是給予了C++的原語實現,接下來呢再看bytecodeInterpreter.cpp截取的代碼,會再給予一個OrderAccess::storeload()操做,而這個操做執行的代碼是這樣的(orderAccess_linux_x86.inline.hpp):

inline void OrderAccess::storeload()  { fence(); }

fence方法代碼以下:

複製代碼
inline void OrderAccess::fence() {
  if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  }
}
複製代碼

同樣能夠看到和經過-XX:+PrintAssembly來看到的背後實現:lock; addl,其實這個就是內存屏障,關於內存屏障的詳細說明能夠看下orderAccess.hpp的註釋。內存屏障提供了3個功能:確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;強制將對緩存的修改操做當即寫入主存;若是是寫操做,它會致使其餘CPU中對應的緩存行無效。這3個功能又是怎麼作到的呢?來看下內存屏障的策略:

在每一個volatile寫操做前面插入storestore屏障;

在每一個volatile寫操做後面插入storeload屏障;

在每一個volatile讀操做後面插入loadload屏障;

在每一個volatile讀操做後面插入loadstore屏障;

其中loadload和loadstore對應的是方法acquire,storestore對應的是方法release,storeload對應的是方法fence。

4、volatile應用場景

 4.1 double check單例

 1 public class Singleton {
 2     private static volatile  Singleton instance;
 3     private Singleton() {};
 4     public static Singleton getInstance() {
 5         if (instance == null) {
 6             synchronized (Singleton.class) {
 7                 if (instance == null) {
 8                     instance = new Singleton();
 9                 }
10             }
11         }
12         return instance;
13     }
14 }

爲何要這樣寫,這個網上有不少資料,這裏就不贅述了。

4.2 java.util.concurrent

大量的應用在j.u.c下的各個基礎類和工具欄,構成Java併發包的基礎。後續併發編程的學習就能夠按照這個路線圖來學習了。

參考資料:

https://github.com/lingjiango/ConcurrentProgramPractice

https://stackoverflow.com/questions/4885570/what-does-volatile-mean-in-java

https://stackoverflow.com/questions/106591/do-you-ever-use-the-volatile-keyword-in-java

http://www.javashuo.com/article/p-kcqqguuv-ba.html

http://download.oracle.com/otn-pub/jcp/memory_model-1.0-pfd-spec-oth-JSpec

https://www.cs.umd.edu/~pugh/java/memoryModel/

相關文章
相關標籤/搜索