1.Java語言規範第3版中對volatile的定義以下:java
Java編程語言容許線程訪問共享變量,爲了確保共享變量可以被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖要更加方便。若是一個字段被聲明成volatile,Java線程內存模型確保全部線程看到這個變量的值一致。編程
2.通俗理解:緩存
volatile就是Java的一個關鍵字,單詞volatile自己具備不穩定的意思。volatile關鍵字表示被修飾的變量的值容易變化,不穩定。volatile變量的不穩定性意味着對這種變量的讀和寫操做都必須從高速緩存或者主內存中讀取,以讀取變量相對新的值。多線程
1. 原理:volatile關鍵字在原子性方面僅保障對被修飾的變量的讀操做、寫操做自己的原子性,若是要保障對volatile變量的賦值操做的原子性,那麼這個賦值操做不能涉及任何共享變量(包括被賦值的volatile變量自己)的訪問。併發
例子1:num1=num2+1;編程語言
若是變量num2也是一個共享變量,那麼賦值操做其實是一個read-modify-write操做。其執行過程當中其餘線程可能已經更新了num2的值,所以該操做不具有不可分割性,也就不是原子操做。若是變量num2是一個局部變量,那麼賦值操做就是一個原子操做。spa
例子2:volatile Map map =new HashMap();線程
該操做能夠分解爲以下僞代碼所示的幾個子操做:對象
objRef = alllocate(HashMap.class); // 子操做(1) : 分配對象所需的存儲空間 invokeConstructor(objRef); // 子操做(2) : 初始化objRef引用的對象 aMap = objRef; // 子操做(3) : 將對象引用寫入變量aMap
雖然volatile關鍵字僅保障其中的子操做(3)是一個原子操做,可是因爲子操做(1)和子操做(2)僅涉及局部變量而未涉及共享變量,所以對變量aMap的賦值操做仍然是一個原子操做.blog
2.在Java語言中對long型和double型之外的任何類型的變量的寫操做都是原子操做。考慮到32位Java虛擬機上對long/double型變量進行的寫操做可能不具備原子性。Java語言規範特別的規定對long/double型volatile變量的寫操做和讀操做也具備原子性。
那麼,爲何32位Java虛擬機上對long/double型變量進行的寫操做可能不具備原子性呢?
Java中long/double型變量會佔用64位的存儲空間,而32位的Java虛擬機對這種變量的寫操做可能會被分解爲兩個步驟來實施,好比先寫低32位,再寫高32位。那麼在多個線程試圖共享同一個這樣的變量時就可能出現一個線程在寫高32位的時候,另外一個線程正在寫低32位。因此最終結果可能就是一個線程對64位的long/double的低32位與另外一個線程對該變量的高32位進行更新所混合出來的一個結果。
1.原理:Java內存屏障保障了讀線程對寫線程在更新volatile變量前對共享變量所執行的更新操做的感知順序與相應的源代碼順序一致,即保障了有序性。
2.JMM如何實現volatile寫、讀的內存語義:JMM經過限制重排序來保障有序性,重排序分爲編譯器重排序和處理器重排序。
2.1 JMM限制編譯器對volatile重排序
表2-2 JMM針對編譯器制定的volatile重排序規則表
舉例:對於第一個操做是普通讀/寫,第二個操做是volatile寫,則編譯器不能重排序這兩個操做。
總結以上表格:
2.2 JMM限制處理器對volatile重排序
爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
下面是基於保守策略的JMM內存屏障插入策略:
2.2-1 volatile寫插入內存屏障後生成的指令序列示意圖 2.2-2 volatile讀插入內存屏障後生成的指令序列示意圖
下面是4種屏障做用:
StoreStore屏障:保障上面全部的普通寫在volatile寫以前刷新到主內存。
StoreLoad屏障:避免volatile寫與後面可能有的volatile讀/寫操做重排序。
LoadLoad屏障:禁止處理器把上面的volatile寫與下面的普通讀重排序。
LoadStore屏障:禁止處理器把上面的volatile讀與下面的普通寫重排序。
有volatile修飾的共享變量進行寫操做時彙編代碼會多出Lock指令。
Lock前綴的指令在多核處理器具備如下做用:
1.將當前處理器緩存行的數據寫回到系統內存中。Lock前綴指令致使在執行指令期間,聲言處理器的LOCK#信號,在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器能夠獨佔任何共享內存。
2.這個寫回的操做會使其餘在CPU裏緩存了該內存地址的數據無效。處理器可以使用嗅探技術保證它的內部緩存、系統內存與其餘處理器的緩存的數據在總線上保持一致性。
注意:volatile關鍵字在可見性方面僅僅是保證讀線程可以讀取到共享變量的相對新值,對於引用型變量,volatile關鍵字並不能保證線程可以讀取到相應對象的字段(實例變量、靜態變量)、元素的相對新值。
volatile的讀、寫操做都不會致使上下文切換,所以volatile的開銷比鎖要小。
寫一個volatile變量會使該操做以及該操做以前的任何寫操做的結果對其餘處理器是可同步的,所以volatile變量寫操做的成本介於普通變量的寫操做和在臨界區內進行的寫操做之間。
volatile變量讀操做的成本也介於普通變量的寫操做和在臨界區內進行的寫操做之間。由於volatile變量的值每次都須要從高速緩存或者主內存中讀取,而沒法被暫存在寄存器中,從而沒法發揮訪問的高效性。
1.使用volatile變量做爲狀態標誌。應用程序的某個狀態由一個線程設置,其餘線程會讀取該狀態並以該狀態做爲其計算的依據。此時使用volatile的好處是一個線程可以‘通知’另一個線程某種事件的發生,而這些線程又無須所以而使用鎖,從而避免了使用鎖的開銷。
2.使用volatile保障可見性。在該場景中,多個線程共享一個可變狀態變量,其中一個線程更新了該變量以後,其餘線程無須加鎖的狀況下也可以看到該更新。
3.使用volatile變量替代鎖。volatile變量並不是鎖的替代品,可是在必定的條件下他比鎖更合適。多個線程共享一組可變狀態變量的時候,咱們能夠把一組可變狀態變量封裝成一個對象,那麼對這些狀態變量的更新操做就能夠經過建立一個新的對象並將該對象引用賦值給相應的引用型變量來實現。
4.使用volatile實現建議版讀寫鎖。這種簡易版讀寫鎖僅涉及一個共享變量而且僅容許一個線程讀取這個共享變量時其餘線程能夠更新該變量。所以,這種讀寫鎖容許讀線程能夠讀取
參考:
《Java併發編程的藝術》
《Java多線程編程實戰指南》