以前的文章,咱們講到volatile的一些做用數組
今天咱們來分析一下volatile的具體用法和內存可見性/讀寫原子性的實現原理緩存
咱們先來看看volatile的使用場景安全
可使用volatile的狀況包括:併發
咱們來經過2個例子說明這些狀況性能
class VolatileFeatures { long vl = 0L; // 64位的long型普通變量 //對單個的普通 變量的寫用同一個鎖同步 public synchronized void set(long l) { vl = l; } public void getAndIncrement () { //普通方法調用 long temp = get(); //調用已同步的讀方法 temp += 1L; //普通寫操做 set(temp); //調用已同步的寫方法 } public synchronized long get() { //對單個的普通變量的讀用同一個鎖同步 return vl; } }
上面的例子中對值的修改須要依賴當前值,可是當前值可能會同時修改,從而出錯測試
public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
上述代碼中,上下界初始化分別爲0和10,假設線程A和B在某一時刻同時執行了setLower(8)和setUpper(5), 且都經過了不變式的檢查,設置了一個無效範圍(8, 5),優化
因此在這種場景下是沒法保證線程安全的,須要經過sychronize保證方法setLower和setUpper在每一時刻只有一個線程可以執行。this
簡單的來講寫入volatile 變量的這些有效值須要獨立於任何程序的狀態,包括變量的當前狀態.net
常見的使用場景線程
public class ServerHandler { private volatile isopen; public void run() { if (isopen) { } else { } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
在併發場景中經過volatile來控制isopen在控制線程的執行邏輯
當只有一個線程能夠修改字段的值,其它線程能夠隨時讀取,那麼把字段聲明爲volatile也是合理的。
須要注意的是聲明一個引用變量爲volatile,不能保證經過該引用變量訪問到的非volatile變量的可見性。同理,聲明一個數組變量爲volatile不能確保數組內元素的可見性。volatile的特性不能在數組內傳遞,由於數組裏的元素不能被聲明爲volatile
下面咱們來分析一下volatile的實現原理,如何保證內存的可見性和讀寫的原子性
咱們經過觀察volatile變量和普通變量所生成的彙編代碼能夠發現,操做volatile變量會多出一個lock前綴指令:
Java代碼: private volatile Singleton instance = new Singleton(); 彙編代碼: 0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
在這裏這個lock前綴指令,提供瞭如下保證:
CPU爲了提升處理性能,並不直接和內存進行通訊,而是將內存的數據讀取到內部緩存再進行操做,但操做完並不能肯定什麼時候寫回到內存,但對volatile變量進行寫操做,當CPU執行到Lock前綴指令時,會將這個變量所在緩存行的數據寫回到內存,但其它CPU緩存的仍是舊值,因此爲了保證各個CPU的緩存一致性,每一個CPU經過檢測在總線上傳播的數據來檢查本身緩存的數據有效性,當發現本身緩存行對應的內存地址的數據被修改,就會將該緩存行設置成無效狀態,當CPU讀取該變量時,發現所在的緩存行被設置爲無效,就會從新從內存中讀取數據到緩存中。
這裏能夠參考咱們以前的文章對cpu的原子性實現的分析
在使用volatile的時候咱們還會遇到僞共享的問題
那麼什麼是僞共享問題
咱們首先要知道,cpu緩存加載的時候一次性最少會加載64個字節(64位處理器),這意味着若是一個隊列的頭節點和尾節點都不足64字節的話,處理器會將它們都讀到同一個高速緩存行中,在多處理器下每一個處理器都會緩存一樣的頭尾節點,當一個處理器試圖修改頭接點時會將整個緩存行鎖定,那麼在緩存一致性機制的做用下,會致使其餘處理器要從新加載緩存,而隊列的入隊和出隊操做是須要不停修改頭接點和尾節點,因此在多處理器的狀況下將會嚴重影響到隊列的入隊和出隊效率。咱們可使用追加到64字節的方式來填滿高速緩衝區的緩存行,避免頭接點和尾節點加載到同一個緩存行,使得頭尾節點在修改時不會互相鎖定,從而解決僞共享的問題。
下面咱們經過一個例子來講明這個問題
/** * 僞共享優化 * */ public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { Thread.sleep(1000); System.out.println("starting...."); if (args.length == 1) { NUM_THREADS = Integer.parseInt(args[0]); } longs = new VolatileLong[NUM_THREADS]; for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } final long start = System.currentTimeMillis(); runTest(); System.out.println("duration = " + (System.currentTimeMillis() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } @Contended //JDK8 默認支持自動填充加上這個註解 而且加上虛擬機參數-XX:-RestrictContended public final static class VolatileLong { public volatile long value = 0L; //64位系統默認對象頭12字節(開啓壓縮) 補充10個字節的無用對象讓緩存行共享失效 public long p1, p2, p3, p4, p5, p6,p7,p8,p9,p10; // 這行代碼註釋掉速度就慢很多 } }
分別測試添加一些無用字節來填充緩存行,和不填充,發現速度上差了很多,這就是僞共享帶來的問題
那麼是否是在使用Volatile變量時都應該追加到64字節呢?在兩種場景下不該該使用這種方式。
第一:緩存行非64字節寬的處理器,如P6系列和奔騰處理器高速緩存行是32個字節寬。
第二:共享變量不會被頻繁的寫。由於使用追加字節的方式須要處理器讀取更多的字節到高速緩衝區,這自己就會帶來必定的性能消耗,共享變量若是不被頻繁寫的話,鎖的概率也很是小,就不必追加字節
本文咱們探討了volatile的具體用法和 volatile經過CPU的Lock指令來保證內存可見性/讀寫原子性的實現原理 咱們還討論了緩存行引發的僞共享問題和解決方案,
其中省略了volatile引發的重排序內容等咱們將在後面的JVM內存模型文章中繼續探討