目錄html
Java語言規範第三版中對volatile的定義以下:java編程語言容許線程訪問共享變量,爲了確保共享變量能被準確和一致的更新,線程應該確保經過排他鎖單獨得到這個變量。Java語言提供了volatile,在某些狀況下比鎖更加方便。若是一個字段被聲明成volatile,java線程內存模型確保全部線程看到這個變量的值是一致的。
volatile變量修飾符若是使用恰當的話,它比synchronized的使用和執行成本會更低,由於它不會引發線程上下文的切換和調度。java
處理器爲了提升處理速度,不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存L1,L2或其餘)後再進行操做,但操做完以後不知道什麼時候會寫到內存,若是對聲明瞭Volatile變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題,因此在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。c++
LOCK前綴指令會引發緩存回寫到內存,LOCK前綴指令致使執行指令期間,聲明處理器的LOCK#信號。在多處理器環境中,LOCK# 信號確保在聲言該信號期間,處理器能夠獨佔使用任何共享內存。(由於它會鎖住總線,致使其餘CPU不能訪問總線,不能訪問總線就意味着不能訪問系統內存)程序員
LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大。對於Intel486和Pentium處理器,在鎖操做時,老是在總線上聲言LOCK#信號。但在P6和最近的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反地,它會鎖定這塊內存區域的緩存並回寫到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲「緩存鎖定」,緩存一致性機制會阻止同時修改被兩個以上處理器緩存的內存區域數據 。
一個處理器的緩存回寫到內存會致使其餘處理器的緩存無效 。IA-32處理器和Intel 64處理器使用MESI(修改,獨佔,共享,無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32 和Intel 64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。它們使用嗅探技術保證它的內部緩存,系統內存和其餘處理器的緩存的數據在總線上保持一致。例如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來檢測其餘處理器打算寫內存地址,而這個地址當前處理共享狀態,那麼正在嗅探的處理器將無效它的緩存行,在下次訪問相同內存地址時,強制執行緩存行填充。編程
public class VolatileTest { //可見性 private /**volatile**/ static int INIT_VALUE = 0; private final static int MAX_VALUE = 5; public static void main(String[] args) throws InterruptedException { new Thread(() -> { int localVale = INIT_VALUE; while (localVale < MAX_VALUE) { /*** 對INIT_VALUE沒有volatile關鍵字修 ***/ //爲何這裏一直沒有去從主內存中拿數據進行刷新呢? //這是由於java認爲這裏沒有writer的操做,因此不須要去主內存中獲取新的數據。這個具體最新的值被刷新指 //具體能夠VolatileTest2進行比較 //在這裏加一個sysytem的輸出是有可能會去刷新主存的,或者每次運行的時候休眠一小段時間, // 這個程序是有可能會結束的。若是沒有System的輸出,或者休眠,在while判斷會一直不去主內存 //刷新新數據,也就致使程序一直無法結束。 //System.out.println("="); /*try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }*/ if (localVale != INIT_VALUE) { System.out.println("The value updated to [ " + INIT_VALUE + " ]"); localVale = INIT_VALUE; } } },"READER").start(); TimeUnit.SECONDS.sleep(1); new Thread(() -> { int localValue = INIT_VALUE; while (INIT_VALUE < MAX_VALUE) { System.out.println("update the value to [ " + (++localValue) + " ]"); INIT_VALUE = localValue; try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"UPDATER").start(); } }
Java的內存模型具有一些天生的有序規則,不須要任何同步手段就可以保證的有序性,這個規則被稱爲Happens-before原則,若是兩個操做的執行順序沒法從happens-before原則推導出來,那麼它們就沒法保證有序性,也就是說虛擬機或處理器能夠隨意對它們進行重排序處理。緩存
由程序次序規則,在單線程的狀況下,對於指令重排序不會出現什麼問題,可是對於多線程的狀況下,就頗有可能會因爲指令重排序出現問題。
volatile關鍵字直接禁止JVM和處理器對volatile關鍵字修飾的指令重排序,可是對於volatile先後五以來的指令則能夠隨便怎麼排序多線程
int x = 10 int y = 20 /** 在語句volatile int z = 20以前,先執行x的定義仍是先執行y的定義,咱們並不關心,只要可以百分百 保證在執行到z=20的時候,x=0,y=1已經定義好,同理對於x的自增以及y的自增操做都必須在z=20之後才能發生,這個規則能夠認爲是由程序次序規則+volatile規則推導 **/ valatile int z = 20 x++; y++;
private volatile boole init = false; private Context context ; public Context context() { if(!init){ context = loadContext(); /** 若是init不使用volatile關鍵字修飾的話,因爲編譯器會對指令作必定的優化,也就是指令重排序。 因此在由多線程執行的狀況下,如某個線程A它可能執行init = true,後執行context = loadContext(), 由於這兩條指令並無任何的依賴關係,因此執行順序可能不定。當線程B執行到判斷的時候,發現init=true成立, 那麼線程B就不會再去加載context啦,此時若是它使用context,有可能context在線程A中尚未加載成功,此時線程B去 使用context就有可能報空指針異常。 而volatile關鍵字能阻止指令重排序,也就是說在init=true以前必定保證context=loadContext()執行完畢。 **/ init = true; //阻止指令重排序 } }
其實被volatile修飾的變量存在一個「lock」的前綴。
lock前綴實際上至關因而一個內存屏障,該內存屏障會爲指令的執行提供以下幾個保障
1.確保指令重排序不會將其後面的代碼排到內存屏障以前
2.確保指令重排序不會將其前面的代碼拍到內存屏障以後。
3.確保在執行內存屏障修飾的指令時前面的代碼所有執行完成(1,2,3阻止了指令重排序)
4.強制將線程工做內存中的修改刷新至主內存中
5.若是是寫操做,則會致使其餘線程的工做內存(CPU Cache)中的緩存數據失效。(4,5保證了內存可見性)併發
volatile沒法保證原子性
原子性:一個操做或多個操做,要麼都成功,要麼都失敗,中間不能因爲任何的因素中斷
對基本數據類型的變量讀取和賦值是保證了原子性的,要麼都成功,要麼都失敗,這些操做不可被中斷
a = 10; 原子性
b = a; 不知足1.read a; 2.assign to b;
c++; 不知足1.read c; 2.add 3.assign to c
c = c + 1; 不知足1.read c; 2.add 3.assign to capp
public class VolatileTest2 { //雖然保證了可見性,可是沒有保證原子性 private volatile static int INIT_VALUE = 0; private final static int MAX_VALUE = 50; public static void main(String[] args) { new Thread(() -> { while (INIT_VALUE < MAX_VALUE) { //頗有可能會輸出重複的數字 System.out.println("ADD-1-> " + (++INIT_VALUE)); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD-1").start(); new Thread(() -> { while (INIT_VALUE < MAX_VALUE) { System.out.println("ADD-2-> " + (++INIT_VALUE)); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } },"ADD-2").start(); } }
(1) 使用上的區別jvm
併發之volatile底層原理:http://www.javashuo.com/article/p-fgsreuii-a.html