理解volatile 平時工作中對於多線程的應用並不太多,但是不能說工作中不應用就可以對此不去瞭解,至少要做的知道有這麼個東西,主要是作什麼的,這樣有助於看其它人寫的代碼。提到這個volatile,一般都會想到併發,同步,鎖之類,但要想搞清楚需要看看下麵一些知識。 處理器,高速緩存,主記憶體之間的關係 ...
理解volatile
平時工作中對於多線程的應用並不太多,但是不能說工作中不應用就可以對此不去瞭解,至少要做的知道有這麼個東西,主要是作什麼的,這樣有助於看其它人寫的代碼。提到這個volatile,一般都會想到併發,同步,鎖之類,但要想搞清楚需要看看下麵一些知識。
處理器,高速緩存,主記憶體之間的關係
高速緩存的作用是什麼?
由於處理器與主記憶體在處理數據的速度上有數量級的差異,所以引入了比主記憶體速度更快的高速緩存。處理器從主記憶體中讀取數據放到高速緩存中做交互運算,最後回寫到主記憶體中。
引入高速緩存會帶來哪些問題?
- 使電腦系統更加複雜,但相對帶來的優點還是值得的。
- 緩存一致性問題
多個處理器如果操作的是同一個主記憶體中的變數,那麼就會出現以誰為準的問題。這就要靠一些規定的協議來維護。
JAVA線程,工作記憶體,主記憶體之間的關係
這是JAVA記憶體模型範疇,主要是用來屏蔽硬體與系統的記憶體訪問差異,讓JAVA程式可以在不同的平臺上達到相同的記憶體訪問效果。
這裡說的工作記憶體,主記憶體與JVM記憶體中講的JAVA堆,棧,方法區不是同一層次上的概念,需要區分。
記憶體交互操作
主要是工作記憶體與主記憶體之間的具體交互協議,即一個變數是如何從主記憶體載入到工作記憶體,然後從工作記憶體如何同步到主記憶體的具體實現細節,總共有以下幾個操作:
- lock,標識一個變數被某個變更獨占
- write,將工作記憶體中的變數回寫到主記憶體中。這點是volatile的關鍵,它能夠保證被標記了volatile的變數一旦被修改馬上執行回寫主記憶體的操作,從而保證其它線程的可見性。
原子性,可見性,有序性
這三個特性是併發操作中需要處理的問題,volatile與下麵兩個特性有關聯。所以在符合可見性以及有序性特性的場景就是volatile的適用場景,也是它的作用所在。
- 原子性
上面提到的記憶體交互操作中除了兩個鎖相關的都可以認為是原子性操作。 - 可見性
意思是說對於某個共用的變數,一個線程對其做了修改之後其它線程立馬可見。volatile在可見性方面上相對普通的變數有著重大區別,它能夠確保共用變數被修改後馬上執行回寫主記憶體的操作,而普通的變數做不到。 - 有序性
這裡有一個有意思的東西就是重排序,它的大體意思就是編寫的代碼順序不一定就是最終被處理器執行的順序,這是為了處理器內部的運算單元能夠儘量的被充分利用,有興趣的可以仔細研究下。而使用了volatile的變數能夠確保不被重排序,這是與普通變數不同的第二個重要區別。
volatile的適用場景
- 運算的結果不依賴共用變數當前的值。
反例,多線程對volatile靜態變數執行累加。
這裡的count++看起來是一個語句,但對應的位元組碼不是一條,在執行多條位元組碼的期間主記憶體中的值有可能被其它線程所修改,從而導致回寫到記憶體中的值不一致。下麵代碼的輸出並不總是正確。
private static volatile int count=0; private static void increase(){ count++; } public static void main(String[] args) throws Exception { Thread[] threads=new Thread[30]; for(int i=0;i<threads.length;i++){ threads[i]=new Thread(new Runnable() { @Override public void run() { for(int k=0;k<10000;k++){ increase(); System.out.println(count); } } }); threads[i].start(); Thread.sleep(1); } }
下麵是count++對應的位元組碼,很明顯是多條位元組碼。volatile只能保證每次讀取變數的值是最新的,它在獲取到主記憶體變數後是對其副本進行修改,並不會鎖定主記憶體中的值。
static void access$000(); flags: ACC_STATIC, ACC_SYNTHETIC Code: stack=0, locals=0, args_size=0 0: invokestatic #2 // Method increase:()V 3: return LineNumberTable: line 6: 0 static int access$100(); flags: ACC_STATIC, ACC_SYNTHETIC Code: stack=1, locals=0, args_size=0 0: getstatic #1 // Field count:I 3: ireturn LineNumberTable: line 6: 0 static {}; flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: iconst_0 1: putstatic #1 // Field count:I 4: return LineNumberTable: line 8: 0 }
某些共用的狀態變數是非常適合的,比如dubbo提供的accesslog filter。某些資源只載入一次的場景特別適用,比如應用程式的配置文件的載入,變數的初始化之類。
private volatile ScheduledFuture<?> logFuture = null; private void init() { if (logFuture == null) { synchronized (logScheduled) { if (logFuture == null) { logFuture = ...; } } } }
- 也不需要與其它的變數共同參與不變約束
volatile變數與普通的變數在執行性能上的區別
由於volatile需要在修改變數時增加記憶體屏障語句,理所當然的相對沒有記憶體屏障語句的普通變數要慢一些。
volatile與同步語法塊,鎖的區別
volatile的特點是當線程對變數修改後馬上回與記憶體保證可見性,同時禁止重排序保證程式執行的有序性。由於它操作的是副本並不會對主記憶體加鎖,所以並不具體同步語法塊以及鎖的特點即可一時刻同一變數只允許一個線程操作。
引用
本文主要引用《深入理解JAVA虛似機》