理解volatile其實仍是有點兒難度的,它與Java的內存模型有關,因此在理解volatile以前須要先了解有關Java內存模型的概念,目前只作初步的介紹。html
計算機在運行程序時,每條指令都是在CPU中執行的,在執行過程當中勢必會涉及到數據的讀寫。數據庫
咱們知道程序運行的數據是存儲在主存中,這時就會有一個問題,讀寫主存中的數據沒有CPU中執行指令的速度快,若是任何的交互都須要與主存打交道則會大大影響效率,因此就有了CPU高速緩存。緩存
CPU高速緩存爲某個CPU獨有,只與在該CPU運行的線程有關。多線程
有了CPU高速緩存雖然解決了效率問題,可是它會帶來一個新的問題:內存不可見性 ——> 數據一致性。app
線程在運行的過程當中會把主內存的數據拷貝一份到線程內部cache中,也就是working memory。這個時候多個線程訪問同一個變量,其實就是訪問本身的內部cache,再也不與主存打交道,只有當運行結束後纔會將數據刷新到主存中。性能
【舉一個簡單的例子】:atom
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); } } }
運行結果:(光標一直在閃)spa
上面例子出現問題的緣由在於:操作系統
(1)線程A把變量flag(false)加載到本身的內部緩存cache中;線程B修改變量flag後,即便從新寫入主內存,可是線程A不會從新從主內存加載變量flag,看到的仍是本身cache中的變量flag。因此線程A是讀取不到線程B更新後的值,而後一直死循環...線程
(2)除了cache的緣由,重排序後的指令在多線程執行時也有可能致使內存不可見,因爲指令順序的調整,線程A讀取某個變量的時候線程B可能尚未進行寫入操做呢,雖然代碼順序上寫操做是在前面的。
可是方案1存在一個問題:它是採用一種獨佔的方式來實現的,即總線加LOCK#鎖的話,只能有一個CPU可以運行,其餘CPU都得阻塞,效率較爲低下。
第二種方案:緩存一致性協議(MESI協議)確保每一個緩存中使用的共享變量的副本是一致的。
核心思想以下:當某個CPU在寫數據時,若是發現操做的變量是共享變量,則會通知其餘CPU告知該變量的緩存行是無效的,所以其餘CPU在讀取該變量時,發現其無效會從新從主存中加載數據。
共享變量是指:能夠同時被多個線程訪問的變量,共享變量是被存放在堆裏面,全部的方法內的臨時變量都不是共享變量。
二、有序性 (happens-before原則)
一個線程觀察其餘線程中的指令執行順序,因爲指令重排序,該觀察結果通常雜亂無序
重排序:是指爲了提升指令運行的性能,在編譯時或者運行時對指令執行順序進行調整的機制。重排序分爲編譯重排序和運行時重排序。
編譯重排序是指:編譯器在編譯源代碼的時候就對代碼執行順序進行分析,在遵循as-if-serial的原則前提下對源碼的執行順序進行調整。
運行時重排序:是指爲了提升執行的運行速度,系統對機器的執行指令的執行順序進行調整。
【as-if-serial原則】是指在單線程環境下,不管怎麼重排序,代碼的執行結果都是肯定的。
內存的可見性是指線程之間的可見性,一個線程的修改狀態對另一個線程是可見的,用通俗的話說,就是假如一個線程A修改一個共享變量flag以後,則線程B去讀取,必定能讀取到最新修改的flag。
(看上邊示例)
Java提供了volatile來保證可見性。
當一個變量被volatile修飾後,表示着線程本地內存無效,當一個線程修改共享變量後他會當即被更新到主內存中,當其餘線程讀取共享變量時,它會直接從主內存中讀取。
固然,synchronize和鎖均可以保證可見性。
原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操做。 即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。就像數據庫裏面的事務同樣,他們是一個團隊,同生共死。
在單線程環境下咱們能夠認爲整個步驟都是原子性操做,可是在多線程環境下則不一樣,Java只保證了基本數據類型的變量和賦值操做纔是原子性的。
要想在多線程環境下保證原子性,則能夠經過鎖、synchronized來確保。
// 一個很經典的例子就是銀行帳戶轉帳問題:
好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。試想一下,若是這2個操做不具有原子性,會形成什麼樣的後果。 假如從帳戶A減去1000元以後,操做忽然停止(B此時並無收到)。 而後A又從B取出了500元,取出500元以後,再執行 往帳戶B加上1000元 的操做。這樣就會致使帳戶A雖然減去了1000元,可是帳戶B沒有收到這個轉過來的1000元(少了500)。 因此這2個操做必需要具有原子性(不可分割的最小操做單位)才能保證不出現一些意外的問題。
(1)volatile修飾的變量不容許線程內部cache緩存和重排序,能夠保證內存的可見性和數據的一致性。
(2)線程讀取數據的時候直接讀寫內存,同時volatile不會對變量加鎖,所以性能會比synchronized好。
另外還有一個說法是使用volatile的變量依然會被讀到線程內部cache中,只不過當B線程修改了flag後,會將flag寫回主內存,同時會經過信號機制通知到A線程去同步內存中flag的值。
volatile相對於synchronized稍微輕量些,在某些場合它能夠替代synchronized,可是又不能徹底取代synchronized,只有在某些場合纔可以使用volatile。
使用它必須知足以下兩個條件: // 一、對變量的寫操做不依賴當前值; // 二、該變量沒有包含在具備其餘變量的不變式中
volatile常常用於兩個兩個場景:狀態標記、double check(單例模式)