一、介紹
volatile保證共享變量的“可見性”。可見性指的是當一個線程修改變量時,另一個線程能讀到這個修改的值。
這里就要提出幾個問題。
- 問題1:為什么一個線程修改時,另一個線程可能會“看不見”?
- 問題2:這種可見性是如何實現的?
二、問題1 變量為何“不可見”
回答:是由於緩存導致的可見性問題
2.1 為什么要引入緩存?
是為了解決性能問題。CPU的處理速度遠遠快於內存的讀取速度(CPU與內存之間的瓶頸也叫“馮諾依曼瓶頸”),所以處理器不直接和內存通信,而是設置內部緩存(L1、L2...),內存的數據會讀取到內部緩存中,CPU直接從內部緩存中讀取數據處理。
2.2 L1、L2、L3有何區別?
L1、L2、L3是CPU三級緩存,CPU緩存的定義為CPU與內存之間的臨時數據交換器,它的出現是為了解決CPU運行處理速度與內存讀寫速度不匹配的矛盾。
讀取速度 L1 > L2 > L3
容量大小 L1 < L2 < L3
L1又分為一級數據緩存、一級指令緩存,分別用於存放數據、執行數據的指令解碼。
2.3 只管高速緩存中的變量,不管內存中的變量行不行?
多個處理器修改變量寫到高速緩存,高速緩存中存儲的變量值是最新,下次不管內存中的變量值,直接從高速緩存中獲取不就行了?
回答:當然不行。
為什么?因為每個處理器都有自己的高速緩存,一個變量在A處理器的高速緩存中修改,B處理器可能感知不到。
三、問題2 變量如何變為“可見”
為了保證在多處理器下,緩存一致,就設置一個緩存一致的協議。每個處理器通過嗅探在總線傳播的數據來檢查自己緩存的值是否是過期的。
當處理器發現自己的緩存行對內的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當需要對數據進行修改時,重新從內存中讀到高速緩存中。
3.1 具體是如何實現?
當變量被volatile修飾,編譯成匯編時,會增加一個Lock前綴指令。
Lock前綴指令的作用:
- Lock前綴指令,會讓處理器緩存回寫到內存
- 一個處理器緩存回寫到內存,導致其他處理器的緩存無效
volatile不保證原子性的例子
public class VolatileNoAutomicTest {
public static ExecutorService threadPool = new ThreadPoolExecutor(10, 100,
2, TimeUnit.SECONDS,
// 不存儲元素的阻塞隊列
new SynchronousQueue<>(),
new BasicThreadFactory.Builder().namingPattern("thread-%d").build());
public static volatile int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
threadPool.execute(() -> {
for(int j = 1;j<1000;j++){
count++;
}
});
}
threadPool.shutdown();
while (!threadPool.isShutdown()){
}
System.out.println(count);
}
}