理解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虛似機》