在多線程併發編程中synchronized和volatile都扮演者重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。
可見性的意思是當一個線程修改一個共享變量時,另外的線程能讀到這個修改的值。
若是volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度。java
Java語言規範第3版對volatile的定義以下:編程
Java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致地更新,線程應該確保經過排它鎖單獨得到這個變量。緩存
Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,Java線程內存模型確保全部線程看到這個變量的值是一致的。多線程
在瞭解volatile實現原理以前,先看下與實現原理相關的CPU術語與說明架構
術語 | 英文單詞 | 術語描述 |
---|---|---|
內存屏障 | memory barriers | 是一組處理器指令,用戶實現對內存操做的順序限制 |
緩存行 | cache line | CPU高速緩存中能夠分配的最小存儲單位。處理器填寫緩存行時會加載整個緩存行,現代CPU須要執行幾百次CPU指令 |
原子操做 | atomic operations | 不可終端的一個或一系列操做 |
緩存行填充 | cache line fill | 當處理器識別到從內存中讀取操做數是可緩存的,處理器讀取整個高速緩存行到適當的緩存(L1,L2,L3的或全部) |
寫命中 | write hit | 當處理器將操做數寫回到一個內存緩存的區域時,它首先會檢查這個緩存的內存地址是否在緩存行中,若是存在一個有效的緩存行,則處理器將這個操做數寫回到緩存,而不是寫回到內存,這個操做被稱爲寫命中 |
寫缺失 | write misses the cache | 一個有效的緩存行被寫入到不存在的內存區域 |
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1, L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令。 這是volatile實現「可見性」的最重要的一部分併發
Lock前綴的指令在多核處理器下會引起兩件事情編程語言
將當前處理器緩存行的數據寫回到系統內存高併發
在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器能夠獨佔任何共享內存1 。可是,在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線的開銷比較大。
在鎖操做時,會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據性能一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效優化
著名的Java併發編程大師Doug lea在JDK7的併發包裏新增一個隊列集合類LinkedTransferQueue,它在使用volatile變量時,用一種追加字節的方式來優化隊列出隊和入隊的性能。
LinkedTransferQueue的代碼以下
/** 隊列中的頭部節點 **/ private transient final PaddedAtomicReference<QNode> head; /** 隊列中的尾部節點 **/ private transient final PaddedAtomicReference<QNode> tail; static final class PaddedAtomicReference <T> extends AtomicReference <T> { // 使用不少4個字節的引用追加到64字節 Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; PaddedAtomicReference(T r) { super(r); } } public class AtomicReference <V> implements java.io.Serializable { private volatile V value; // 省略其餘代碼 }
追加字節能優化性能? 這種方式看起來很神奇,但若是深刻理解處理器架構就能理解其中的奧祕。對於LinkedTransferQueue這個類,它使用一個內部類類型來定義隊列的頭節點(head)和尾節點(tail),而這個內部類PaddedAtomicReference相對於父類AtomicReference只作了一件事,就是將共享變量追加到64字節。一個對象的引用佔4個字節,它追加了15個變量(共佔60個字節),再加上父類的value變量,一共64個字節。
爲何追加64字節可以提升併發編程的效率呢? 由於對於英特爾酷睿i七、酷睿、Atom和NetBurst,以及Core Solo和Pentium M處理器的L一、L2或L3緩存的高速緩存行是64個字節寬,不支持部分填充緩存行,這意味着,若是隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存中,在多處理器下每一個處理器都會緩存一樣的頭、尾節點,當一個處理器試圖修改頭節點時,會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器不能訪問本身高速緩存中的尾節點,而隊列的入隊和出隊操做則須要不停修改頭節點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。Doug lea使用追加到64字節的方式來填滿高速緩存區的緩存行,避免頭節點和尾節點加載到同一個緩存行,使頭、尾節點在修改時不會互相鎖定。
是否是在使用volatile變量時都應該追加到64字節呢? 不是的。在下面兩種場景下不該該使用這種方式
緩存行非64字節寬的處理器。 如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節寬
共享變量不會被頻繁地寫。 由於使用追加字節的方式須要處理器讀取更多的字節到高速緩存區,這自己就會帶來必定的性能消耗,若是共享變量不被頻繁寫的話,鎖的概率也很是小,就不必經過追加字節的方式來避免相互鎖定。
由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存↩