Java多執行緒之volatile

NO IMAGE

Java多執行緒是一個龐大的知識體系,這裡對其中的volatile進行一個總結,理清他的來龍去脈。

CPU快取

要搞懂volatile,首先得了解CPU在執行過程中的儲存是如何處理的,其結構如圖

clipboard.png

CPU會把一些經常使用的資料快取在cache中,避免每次都去訪問較慢的memory。在單執行緒環境下,如果一個變數的修改都在cache中,自然不會有什麼問題,可是在多執行緒環境中就可能是下面這個圖的示意圖(單核另當別論)

clipboard.png

CPU1 修改了一個變數a存入cache1,但是CPU2 在cache2中看到的a任然是之前的a,所以造成CPU1修改失效,我們來看看示例程式碼:

import java.util.concurrent.TimeUnit;
public class Counter {
private static  boolean stop ;
//private static volatile boolean stop ;
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (!stop) {
i  ;
}
}
} );
t.start();
TimeUnit.MILLISECONDS .sleep(5);
stop = true;
}
}

在我的4核筆記本上執行結果:

clipboard.png

就一直執行著,沒有停止(需要手工停止),這說明在主執行緒中修改的stop變數後,執行緒t沒有讀取到最新的stop的值,還一直是false。

volatile原理

volatile的原理就是,如果CPU1修改了一個變數a,不僅要修改自身的cache,還要同步到memory中去,並且使CPU2的cache中的變數a失效,如果CPU2要讀取a,那麼就必須到memory中去讀取,這樣就保證了不同的執行緒之間對於a的可見性,亦即,無論哪個執行緒,隨時都能獲得變數a最新的最新值。
我們來看看示例程式碼:

import java.util.concurrent.TimeUnit;
public class Counter {
//private static  boolean stop ;
private static volatile boolean stop ;
public static void main(String[] args) throws Exception {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
while (!stop) {
i  ;
}
}
} );
t.start();
TimeUnit.MILLISECONDS .sleep(5);
stop = true;
}
}

在我的4核筆記本上執行結果:

clipboard.png

很快程式就結束了,說明執行緒t讀到了經主執行緒修改後的stop變數,然後就停止了。

(例子源於《effective Java》)

volatile使用場景

狀態標誌

就像上面的程式碼裡,把簡單地volatile變數作為狀態標誌,來達成執行緒之間通訊的目的,省去了用synchronized還要wait,notify或者interrupt的編碼麻煩。

替換重量級鎖

在Java中synchronized 又稱為重量級鎖,能夠保重JMM的幾大特性:一致性,原子性,可見性。但是由於使用了鎖操作,在一定程度上會有更高的效能消耗(鎖的執行緒互斥性亦即資源消耗)。而volatile能提供可見性,原子性(單個變數操作,不是a 這種符合操作),所以在讀寫上,可以用volatile來替換synchronized的讀操作,而寫操作仍然有synchronized實現,能取得更好的效能。

import java.util.ArrayList;
import java.util.List;
public class Counter1 {
private class Count11 {
private  int value;
public synchronized int getValue() {
return value;
}
public synchronized int increment() {
return value  ;
}
}
//    private class Count11 {
//        private volatile int value=0;
//        int getValue() {  return value;    }
//        synchronized int increment() {    return value  ;    }
//    }
public static void main(String[] args) throws Exception {
Counter1.Count11 count11 = new Counter1().new Count11();
List<Thread> threadArrayList = new ArrayList<>();
final int[] a = {0};
Long allTime = 0l;
long startTime = System.currentTimeMillis();
for (int i = 0; i < 4; i  ) {
Thread t = new Thread(() -> {
int b = 0;
for (int j = 0; j < 10000; j  ) {
count11.increment();
a[0] = count11.getValue();
}
for (int j = 0; j < 10000; j  ) {
b  ;
a[0] = count11.getValue();
}
});
t.start();
threadArrayList.add(t);
}
for (Thread t : threadArrayList) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long endTime = System.currentTimeMillis();
allTime = ((endTime - startTime));
System.out.println("result: "   a[0]   ", average time: "   (allTime)   "ms");
}
}

volatile優化結果:

result: 40000, average time: 124ms
result: 40000, average time: 133ms
result: 40000, average time: 141ms
result: 40000, average time: 112ms
result: 40000, average time: 123ms
result: 40000, average time: 143ms
result: 40000, average time: 120ms
result: 40000, average time: 120ms

未優化結果:

result: 40000, average time: 144ms
result: 40000, average time: 150ms
result: 40000, average time: 149ms
result: 40000, average time: 165ms
result: 40000, average time: 134ms
result: 40000, average time: 132ms
result: 40000, average time: 157ms
result: 40000, average time: 138ms
result: 40000, average time: 158ms

可見使用volatile過後效果的確優於只使用synchronized的效能,不過試驗中發現有個閾值,如果讀取修改次數較小,比如1000以內,只使用synchronized效果略好,存取次數變大以後 volatile的優勢才慢慢體現出來(次數達到10000的話,差距就在60ms左右)。

待挖掘

還有很多用法,在將來的學習中,不斷總結與挖掘。

聯想

無論處於應用的哪一層,優化的思路都是可以相互借鑑的,比如我們做一個服務叢集,如果每一個節點都要儲存所有使用者的session,就很難使得session同步,我們就可以借鑑volatile這種思路,在叢集之上搞一個排程器,如果某一個節點修改了一個使用者session,就報告給排程器,然後排程器通知其他所有節點修改該使用者session。而一般情況下,資料的讀寫比都比較高,所以這樣做就能到達一個很好的效能。

注意事項

引用型別的volatile只在引用本身發生變化時具有可見性,其引用的物件的元素髮生變化時不具有可見性

歡迎訪問我的個人主頁 mageek(mageek.cn)