Volatile的實現原理(轉)

看到一篇很好的文章,轉一下。原文地址:http://ifeve.com/volatile/  做者:方騰飛java

 

引言

在多線程併發編程中synchronized和Volatile都扮演着重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。它在某些狀況下比synchronized的開銷更小,本文將深刻分析在硬件層面上Inter處理器是如何實現Volatile的,經過深刻分析能幫助咱們正確的使用Volatile變量。編程

 

術語定義

術語 英文單詞 描述
共享變量   在多個線程之間可以被共享的變量被稱爲共享變量。共享變量包括全部的實例變量,靜態變量和數組元素。他們都被存放在堆內存中,Volatile只做用於共享變量。
內存屏障 Memory Barriers 是一組處理器指令,用於實現對內存操做的順序限制。
緩衝行 Cache line 緩存中能夠分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,須要使用多個主內存讀週期。
原子操做 Atomic operations 不可中斷的一個或一系列操做。
緩存行填充 cache line fill 當處理器識別到從內存中讀取操做數是可緩存的,處理器讀取整個緩存行到適當的緩存(L1,L2,L3的或全部)
緩存命中 cache hit 若是進行高速緩存行填充操做的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操做數,而不是從內存。
寫命中 write hit 當處理器將操做數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,若是存在一個有效的緩存行,則處理器將這個操做數寫回到緩存,而不是寫回到內存,這個操做被稱爲寫命中。
寫缺失 write misses the cache 一個有效的緩存行被寫入到不存在的內存區域。

Volatile的官方定義

Java語言規範第三版中對volatile的定義以下: java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。數組

爲何要使用Volatile

Volatile變量修飾符若是使用恰當的話,它比synchronized的使用和執行成本會更低,由於它不會引發線程上下文的切換和調度。緩存

Volatile的實現原理

那麼Volatile是如何來保證可見性的呢?在x86處理器下經過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操做CPU會作什麼事情。多線程

Java代碼: instance = new Singleton();//instance是volatile變量
彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

有volatile變量修飾的共享變量進行寫操做的時候會多第二行彙編代碼,經過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引起了兩件事情。架構

  • 將當前處理器緩存行的數據會寫回到系統內存。
  • 這個寫回內存的操做會引發在其餘CPU裏緩存了該內存地址的數據無效。

處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。併發

這兩件事情在IA-32軟件開發者架構手冊的第三冊的多處理器管理章節(第八章)中有詳細闡述。jvm

Lock前綴指令會引發處理器緩存回寫到內存。Lock前綴指令致使在執行指令期間,聲言處理器的 LOCK# 信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器能夠獨佔使用任何共享內存。(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存),可是在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節有詳細說明鎖定操做對處理器緩存的影響,對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和最近的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據編程語言

一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32 和Intel 64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。高併發

Volatile的使用優化

著名的Java併發編程大師Doug lea在JDK7的併發包裏新增一個隊列集合類LinkedTransferQueue,他在使用Volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。

追加字節能優化性能?這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其中的奧祕。讓咱們先來看看LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的頭隊列(Head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類AtomicReference只作了一件事情,就將共享變量追加到64字節。咱們能夠來計算下,一個對象的引用佔4個字節,它追加了15個變量共佔60個字節,再加上父類的Value變量,一共64個字節。

01 /** head of the queue */
02 private transient final PaddedAtomicReference<QNode> head;
03  
04 /** tail of the queue */
05 private transient final PaddedAtomicReference<QNode> tail;
06  
07 static final class PaddedAtomicReference <T> extends AtomicReference <T> {
08  
09   // enough padding for 64bytes with 4byte refs
10   Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
11  
12   PaddedAtomicReference(T r) {
13  
14     super(r);
15  
16   }
17  
18 }
19  
20 public class AtomicReference <V> implements java.io.Serializable {
21  
22   private volatile V value;
23  
24   //省略其餘代碼
25  
26

爲何追加64字節可以提升併發編程的效率呢? 由於對於英特爾酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M處理器的L1,L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行,這意味着若是隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做是須要不停修改頭接點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea使用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。

那麼是否是在使用Volatile變量時都應該追加到64字節呢?不是的。在兩種場景下不該該使用這種方式。第一:緩存行非64字節寬的處理器,如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬。第二:共享變量不會被頻繁的寫。由於使用追加字節的方式須要處理器讀取更多的字節到高速緩衝區,這自己就會帶來必定的性能消耗,共享變量若是不被頻繁寫的話,鎖的概率也很是小,就不必經過追加字節的方式來避免相互鎖定。

參考資料

引言

在多線程併發編程中synchronized和Volatile都扮演着重要的角色,Volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。它在某些狀況下比synchronized的開銷更小,本文將深刻分析在硬件層面上Inter處理器是如何實現Volatile的,經過深刻分析能幫助咱們正確的使用Volatile變量。

 

術語定義

術語 英文單詞 描述
共享變量   在多個線程之間可以被共享的變量被稱爲共享變量。共享變量包括全部的實例變量,靜態變量和數組元素。他們都被存放在堆內存中,Volatile只做用於共享變量。
內存屏障 Memory Barriers 是一組處理器指令,用於實現對內存操做的順序限制。
緩衝行 Cache line 緩存中能夠分配的最小存儲單位。處理器填寫緩存線時會加載整個緩存線,須要使用多個主內存讀週期。
原子操做 Atomic operations 不可中斷的一個或一系列操做。
緩存行填充 cache line fill 當處理器識別到從內存中讀取操做數是可緩存的,處理器讀取整個緩存行到適當的緩存(L1,L2,L3的或全部)
緩存命中 cache hit 若是進行高速緩存行填充操做的內存位置仍然是下次處理器訪問的地址時,處理器從緩存中讀取操做數,而不是從內存。
寫命中 write hit 當處理器將操做數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,若是存在一個有效的緩存行,則處理器將這個操做數寫回到緩存,而不是寫回到內存,這個操做被稱爲寫命中。
寫缺失 write misses the cache 一個有效的緩存行被寫入到不存在的內存區域。

Volatile的官方定義

Java語言規範第三版中對volatile的定義以下: java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。

爲何要使用Volatile

Volatile變量修飾符若是使用恰當的話,它比synchronized的使用和執行成本會更低,由於它不會引發線程上下文的切換和調度。

Volatile的實現原理

那麼Volatile是如何來保證可見性的呢?在x86處理器下經過工具獲取JIT編譯器生成的彙編指令來看看對Volatile進行寫操做CPU會作什麼事情。

Java代碼: instance = new Singleton();//instance是volatile變量
彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock addl $0x0,(%esp);

有volatile變量修飾的共享變量進行寫操做的時候會多第二行彙編代碼,經過查IA-32架構軟件開發者手冊可知,lock前綴的指令在多核處理器下會引起了兩件事情。

  • 將當前處理器緩存行的數據會寫回到系統內存。
  • 這個寫回內存的操做會引發在其餘CPU裏緩存了該內存地址的數據無效。

處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。

這兩件事情在IA-32軟件開發者架構手冊的第三冊的多處理器管理章節(第八章)中有詳細闡述。

Lock前綴指令會引發處理器緩存回寫到內存。Lock前綴指令致使在執行指令期間,聲言處理器的 LOCK# 信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器能夠獨佔使用任何共享內存。(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存),可是在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。在8.1.4章節有詳細說明鎖定操做對處理器緩存的影響,對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和最近的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據

一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32 和Intel 64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。

Volatile的使用優化

著名的Java併發編程大師Doug lea在JDK7的併發包裏新增一個隊列集合類LinkedTransferQueue,他在使用Volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。

追加字節能優化性能?這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其中的奧祕。讓咱們先來看看LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的頭隊列(Head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類AtomicReference只作了一件事情,就將共享變量追加到64字節。咱們能夠來計算下,一個對象的引用佔4個字節,它追加了15個變量共佔60個字節,再加上父類的Value變量,一共64個字節。

01 /** head of the queue */
02 private transient final PaddedAtomicReference<QNode> head;
03  
04 /** tail of the queue */
05 private transient final PaddedAtomicReference<QNode> tail;
06  
07 static final class PaddedAtomicReference <T> extends AtomicReference <T> {
08  
09   // enough padding for 64bytes with 4byte refs
10   Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
11  
12   PaddedAtomicReference(T r) {
13  
14     super(r);
15  
16   }
17  
18 }
19  
20 public class AtomicReference <V> implements java.io.Serializable {
21  
22   private volatile V value;
23  
24   //省略其餘代碼
25  
26

爲何追加64字節可以提升併發編程的效率呢? 由於對於英特爾酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M處理器的L1,L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行,這意味着若是隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做是須要不停修改頭接點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea使用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定。

那麼是否是在使用Volatile變量時都應該追加到64字節呢?不是的。在兩種場景下不該該使用這種方式。第一:緩存行非64字節寬的處理器,如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬。第二:共享變量不會被頻繁的寫。由於使用追加字節的方式須要處理器讀取更多的字節到高速緩衝區,這自己就會帶來必定的性能消耗,共享變量若是不被頻繁寫的話,鎖的概率也很是小,就不必經過追加字節的方式來避免相互鎖定。

參考資料

相關文章
相關標籤/搜索