上次咱們已經學習了Synchronized關鍵字,有興趣的能夠看看:死磕Synchronized實現原理,今天來研究一下volatile關鍵字,主要有如下知識點:html
併發編程中的三個概念
深刻剖析volatile關鍵字
volatile的原理和實現機制
使用volatile關鍵字的場景
在併發編程中,咱們一般會遇到如下三個問題:原子性問題,可見性問題,有序性問題。c++
原子性
一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。編程
一個很經典的例子就是銀行帳戶轉帳問題:好比從帳戶A向帳戶B轉1000元,那麼必然包括2個操做:從帳戶A減去1000元,往帳戶B加上1000元。試想一下,若是這2個操做不具有原子性,會形成什麼樣的後果。假如從帳戶A減去1000元以後,操做忽然停止。 這樣就會致使帳戶A雖然減去了1000元,可是帳戶B沒有收到這個轉過來的1000元。因此這2個操做必需要具有原子性才能保證不出現一些意外的問題。
可見性
當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。segmentfault
舉個簡單的例子,看下面這段代碼:緩存
//線程1執行的代碼 int i = 0; i = 10; //線程2執行的代碼 j = i;
倘若執行線程1的是CPU1,執行線程2的是CPU2。當線程1執行 i = 10這句時,會先把i的初始值加載到CPU1的高速緩存中,而後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有當即寫入到內存當中。此時線程2執行 j = i,它會先去內存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值仍是0,那麼就會使得j的值爲0,而不是10。
這就是可見性問題,線程1對變量i修改了以後,線程2沒有當即看到線程1修改的值。多線程
有序性
程序執行的順序按照代碼的前後順序執行。併發
首先解釋一下什麼是指令重排序
,通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。性能
雖然重排序不會影響單個線程內程序執行的結果,可是多線程呢?下面看一個例子:學習
//線程1: context = loadContext(); //語句1 inited = true; //語句2 //線程2: while(!inited ){ sleep() } doSomethingwithconfig(context);
上面代碼中,因爲語句1和語句2沒有數據依賴性,所以可能會被重排序。
假如發生了重排序,在線程1執行過程當中先執行語句2,而此時線程2會覺得初始化工做已經完成,
那麼就會跳出while循環,去執行doSomethingwithconfig(context)方法,而此時context並無被初始化,就會致使程序出錯。
從上面能夠看出,指令重排序不會影響單個線程的執行,可是會影響到線程併發執行的正確性。
也就是說,要想併發程序正確地執行,必需要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會致使程序運行不正確。優化
內存可見性
即線程A對volatile變量的修改,其餘線程獲取的volatile變量都是最新的。
對於volatile修飾的變量(共享變量)來講,在工做內存發生了變化後,必需要立刻寫到主內存中,而線程讀取到是volatile修飾的變量時,必須去主內存中去獲取最新的值,而不是讀工做內存中主內存的副本,這就有效的保證了線程之間變量的可見性。
能夠禁止指令重排序
那麼什麼是指令重排序?
爲了儘量減小內存操做速度遠慢於CPU運行速度所帶來的CPU空置的影響,虛擬機會按照本身的一些規則將程序編寫順序打亂。
volatile能夠保證線程的可見性和有序性,但沒辦法保證對變量的操做的原子性。
在Java中,對基本數據類型的變量的讀取和賦值操做是原子性操做,即這些操做是不可被中斷的,要麼執行,要麼不執行。
請分析如下哪些操做是原子性操做:
x = 10; //語句1 y = x; //語句2 x++; //語句3 x = x + 1; //語句4
只有語句1是原子性操做,其餘三個語句都不是原子性操做。
語句1是直接將數值10賦值給x,也就是說線程執行這個語句的會直接將數值10寫入到工做內存中。
語句2實際上包含2個操做,它先要去讀取x的值,再將x的值寫入工做內存,雖然讀取x的值以及將x的值寫入工做內存這2個操做都是原子性操做,可是合起來就不是原子性操做了。
一樣的,x++和 x = x+1包括3個操做:讀取x的值,進行加1操做,寫入新的值。
下面看一個例子:
public class Test { public volatile int inc = 0; public void increase() { inc++; } public static void main(String[] args) { final Test test = new Test(); for(int i=0;i<10;i++){ new Thread(){ public void run() { for(int j=0;j<1000;j++) test.increase(); }; }.start(); } while(Thread.activeCount()>1) //保證前面的線程都執行完 Thread.yield(); System.out.println(test.inc); } }
事實上運行它會發現每次運行結果都不一致,都是一個小於10000的數字。上面的程序錯在沒能保證原子性。
下面這段話摘自《深刻理解Java虛擬機》:
觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令。
lock前綴指令實際上至關於一個內存屏障,內存屏障會提供3個功能:
- 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
- 它會強制將對緩存的修改操做當即寫入主存;
- 若是是寫操做,它會致使其餘CPU中對應的緩存行無效。
synchronized關鍵字是防止多個線程同時執行一段代碼,那麼就會很影響程序執行效率,而volatile關鍵字在某些狀況下性能要優於synchronized,可是要注意volatile關鍵字是沒法替代synchronized關鍵字的,由於volatile關鍵字沒法保證操做的原子性。
一般來講,使用volatile必須具有如下2個條件:
事實上,個人理解就是上面的2個條件須要保證操做是原子性操做,才能保證使用volatile關鍵字的程序在併發時可以正確執行。