前言:
對於java開發工程師來說,併發編程一直是一個具有挑戰性的技術,本章將給大家介紹一下volatile的原理。
概念
- 共享變量:共享變量是指可以同時被多個線程訪問的變量,共享變量是被存放在堆裏面,所有的方法內臨時變量都不是共享變量。
- 重排序:重排序是指爲了提高指令運行的性能,在編譯時或者運行時對指令執行順序進行調整的機制。重排序分爲編譯重排序和運行時重排序。編譯重排序是指編譯器在編譯源代碼的時候就對代碼執行順序進行分析,在遵循as-if-serial的原則前提下對源碼的執行順序進行調整。as-if-serial原則是指在單線程環境下,無論怎麼重排序,代碼的執行結果都是確定的。運行時重排序是指爲了提高執行的運行速度,系統對機器的執行指令的執行順序進行調整。
- 可見性:內存的可見性是指線程之間的可見性,一個線程的修改狀態對另外一個線程是可見的,用通俗的話說,就是假如一個線程A修改一個共享變量flag之後,則線程B去讀取,一定能讀取到最新修改的flag。
4.說到這裏,可能有些同學會覺得,這不是廢話嗎,線程A修改變量flag後,線程B肯定是可以拿到最新的值的呀。假如你真的這麼認爲,那麼請運行一下以下的代碼:
package test;
public class VariableTest {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
ThreadA threadA = new ThreadA();
ThreadB threadB = new ThreadB();
new Thread(threadA, "threadA").start();
Thread.sleep(1000l);//爲了保證threadA比threadB先啓動,sleep一下
new Thread(threadB, "threadB").start();
}
static class ThreadA extends Thread {
public void run() {
while (true) {
if (flag) {
System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
break;
}
}
}
}
static class ThreadB extends Thread {
public void run() {
flag = true;
System.out.println(Thread.currentThread().getName() + " : flag is " + flag);
}
}
}
- 運行結果-():
以上運行結果證明:線程B修改變量flag之後,線程A讀取不到,A線程一直在運行,無法停止。
解析:
內存不可見的兩個原因:
1、cache機制導致內存不可見
我們都知道,CPU的運行速度是遠遠高於內存的讀寫速度的,爲了不讓cpu爲了等待讀寫內存數據,現代cpu和內存之間都存在一個高速緩存cache(實際上是一個多級寄存器),如下圖:
線程在運行的過程中會把主內存的數據拷貝一份到線程內部cache中,也就是working memory。這個時候多個線程訪問同一個變量,其實就是訪問自己的內部cache。
上面例子出現問題的原因在於:線程A把變量flag加載到自己的內部緩存cache中,線程B修改變量flag後,即使重新寫入主內存,但是線程A不會重新從主內存加載變量flag,看到的還是自己cache中的變量flag。所以線程A是讀取不到線程B更新後的值。
2、除了cache的原因,重排序後的指令在多線程執行時也有可能導致內存不可見,由於指令順序的調整,線程A讀取某個變量的時候線程B可能還沒有進行寫入操作呢,雖然代碼順序上寫操作是在前面的。
volatile的原理:
volatile修飾的變量不允許線程內部cache緩存和重排序,線程讀取數據的時候直接讀寫內存,同時volatile不會對變量加鎖,因此性能會比synchronized好。另外還有一個說法是使用volatile的變量依然會被讀到cache中,只不過當B線程修改了flag之後,會將flag寫回主內存,同時會通過信號機制通知到A線程去同步內存中flag的值。我更傾向於後者的解釋,還望大神指導一下正確的答案。
但是需要注意的是,volatile不保證操作的原子性,請勿使用volatile來進行原子性操作。