對於java開發工程師來講,併發編程一直是一個具備挑戰性的技術,本章將給你們介紹一下volatile的原理。spa
下面介紹幾個概念:線程
共享變量:共享變量是指能夠同時被多個線程訪問的變量,共享變量是被存放在堆裏面,全部的方法內臨時變量都不是共享變量。code
重排序:重排序是指爲了提升指令運行的性能,在編譯時或者運行時對指令執行順序進行調整的機制。重排序分爲編譯重排序和運行時重排序。編譯重排序是指編譯器在編譯源代碼的時候就對代碼執行順序進行分析,在遵循as-if-serial的原則前提下對源碼的執行順序進行調整。as-if-serial原則是指在單線程環境下,不管怎麼重排序,代碼的執行結果都是肯定的。運行時重排序是指爲了提升執行的運行速度,系統對機器的執行指令的執行順序進行調整。
可見性:內存的可見性是指線程之間的可見性,一個線程的修改狀態對另一個線程是可見的,用通俗的話說,就是假如一個線程A修改一個共享變量flag以後,則線程B去讀取,必定能讀取到最新修改的flag。
說到這裏,可能有些同窗會以爲,這不是廢話嗎,線程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線程一直在運行,沒法中止。
內存不可見的兩個緣由:
一、cache機制致使內存不可見
咱們都知道,CPU的運行速度是遠遠高於內存的讀寫速度的,爲了避免讓cpu爲了等待讀寫內存數據,現代cpu和內存之間都存在一個高速緩存cache(其實是一個多級寄存器),以下圖:
線程在運行的過程當中會把主內存的數據拷貝一份到線程內部cache中,也就是working memory。這個時候多個線程訪問同一個變量,其實就是訪問本身的內部cache。
上面例子出現問題的緣由在於:線程A把變量flag加載到本身的內部緩存cache中,線程B修改變量flag後,即便從新寫入主內存,可是線程A不會從新從主內存加載變量flag,看到的仍是本身cache中的變量flag。因此線程A是讀取不到線程B更新後的值。
二、除了cache的緣由,重排序後的指令在多線程執行時也有可能致使內存不可見,因爲指令順序的調整,線程A讀取某個變量的時候線程B可能尚未進行寫入操做呢,雖然代碼順序上寫操做是在前面的。
volatile的原理:
volatile修飾的變量不容許線程內部cache緩存和重排序,線程讀取數據的時候直接讀寫內存,同時volatile不會對變量加鎖,所以性能會比synchronized好。另外還有一個說法是使用volatile的變量依然會被讀到cache中,只不過當B線程修改了flag以後,會將flag寫回主內存,同時會經過信號機制通知到A線程去同步內存中flag的值。我更傾向於後者的解釋,還望大神指導一下正確的答案。
可是須要注意的是,volatile不保證操做的原子性,請勿使用volatile來進行原子性操做。