在Java相關的職位面試中,不少Java面試官都喜歡考察應聘者對Java併發的瞭解程度,以volatile關鍵字爲切入點,每每會問到底,Java內存模型(JMM)和Java併發編程的一些特色都會被牽扯出來,再深刻的話還會考察JVM底層實現以及操做系統的相關知識。java
接下來讓咱們在一個假想的面試過程當中來學習一下volitile關鍵字吧。面試
參考答案:編程
個人理解是,被volatile修飾的共享變量,就會具備如下兩個特性:緩存
參考答案:多線程
這個要是提及來可就多了,我就從Java內存模型開始提及吧。Java虛擬機規範試圖定義一個Java內存模型(JMM),以屏蔽全部類型的硬件和操做系統內存訪問差別,讓Java程序在不一樣的平臺上可以達到一致的內存訪問效果。簡單地說,因爲CPU執行指令的速度很快,可是內存訪問速度很慢,差別不是一個量級,因此搞處理器的那羣大佬們又在CPU里加了好幾層高速緩存。併發
在Java內存模型中,對上述優化進行了一波抽象。JMM規定全部的變量都在主內存中,相似於上面提到的普通內存,每一個線程又包含本身的工做內存,爲了便於理解能夠當作CPU上的寄存器或者高速緩存。所以,線程的操做都是以工做內存爲主,它們只能訪問本身的工做內存,而且在工做以前和以後,該值被同步回主內存。app
說的我本身都有點暈了,用一張圖來幫助咱們理解吧:學習
線程執行的時候,將首先從主內存讀值,再load到工做內存中的副本中,而後傳給處理器執行,執行完畢後再給工做內存中的副本賦值,隨後工做內存再把值傳回給主存,主存中的值才更新。優化
使用工做內存和主存,雖然加快了速度,但也帶來了一些問題。例如:操作系統
i = i + 1;
假設 i 的初始值爲 0 ,當只有一個線程執行它的時候,結果確定是 1 ,那麼當兩個線程執行時,獲得的結果會是 2 嗎?不必定。可能會存在這種狀況:
線程1: load i from 主存 // i = 0 i + 1 // i = 1 線程2: load i from主存 // 由於線程1還沒將i的值寫回主存,因此i仍是0 i + 1 //i = 1 線程1: save i to 主存 線程2: save i to 主存
若是兩個線程遵循上面的執行過程,那麼 i 的最終值居然是 1 。若是最後的寫回生效的慢,你再讀取 i 的值,均可能會是 0 ,這就是緩存不一致的問題。
接下來就要提到您剛纔所問的問題了,JMM主要圍繞在併發過程當中如何處理併發原子性、可見性和有序性這三個特徵來創建的,經過解決這三個問題,就能夠解決緩存不一致的問題。而volatile跟可見性和有序性都有關。
1 . 原子性(Atomicity):
在Java中,對基本數據類型的讀取和賦值操做是原子性操做,所謂原子性操做就是指這些操做是不可中斷的,要作必定作完,要麼就沒有執行。 例如:
i = 2; j = i; i++; i = i + 1;
以上四個操做, i = 2 是一個讀取操做,確定是原子性操做, j = i 你以爲是原子性操做,但事實上,能夠分爲兩個步驟,一個是讀取 i 的值,而後再把值賦給 j ,這已是兩步操做了,不能稱爲原子操做,i++
和 i = i + 1
是等效的,讀的值,+ 1,而後寫回主存,這是三個步驟的操做了。在上面的例子中,最後一個值可能在各類狀況下,由於它不會知足原子性。
在本例中,只有一個簡單的讀取,賦值是一個原子操做,而且只能被分配給一個數字,使用變量來讀取變量的值的操做。一個例外是,在虛擬機規範中容許64位數據類型(long和double),它被劃分爲兩個32位操做,可是JDK的最新實現實現了原子操做。
JMM只實現基本的原子性,好比上面的i++操做,它必須依賴於同步和鎖定,以確保整個代碼塊的原子性。在釋放鎖以前,線程必須將I的值返回到主內存。
2 . 可見性(Visibility):
說到可見性,Java使用volatile來提供可見性。當一個變量被volatile修改時,它的變化會當即被刷新到主存,當其餘線程須要讀取變量時,它會讀取內存中的新值。普通變量不能保證。
事實上,同步和鎖定也能夠保證可見性。在釋放鎖以前,線程將把共享變量值刷回主內存,可是同步和鎖更昂貴。
3 . 有序性(Ordering)
JMM容許編譯器和處理器從新排序指令,可是指定了as-if-串行語義,也就是說,不管從新排序,程序的執行結果都不能更改。例如:
double pi = 3.14; //A double r = 1; //B double s= pi * r * r;//C
上面的語句中,能夠按照C - > B - >,結果是3.14,但它也能夠按照的順序B - > - > C,由於A和B是兩個單獨的語句,並依賴,B,C和A和B能夠從新排序,但C不能行前面的A和B。JMM確保從新排序不會影響單線程的執行,但容易出現多線程問題。例如,這樣的代碼:
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
若是有兩個線程執行上面的代碼段,線程1首先執行寫,而後再乘以線程2。最後,ret的值必須是4?不必定:
如圖1和2所示,在寫方法中進行從新排序,線程1對第一個賦值爲true,而後執行到線程2,ret直接計算結果,而後再執行線程1,這一次的CaiFu值爲2,顯然是較晚的步驟。
此時要標記加上volatile關鍵字,從新排序,能夠確保程序的「順序」,也能夠基於重量級的同步和鎖定來確保,他們能夠確保在代碼執行的區域內一次性完成。
此外,JMM有一些內在的規律性,也就是說,沒有任何方法能夠保證有序,這一般稱爲發生在原則以前。<< jsr-133: Java內存模型和線程規範>>定義瞭如下事件:
ThreadB_start()
(啓動線程B) , 那麼A線程的ThreadB_start()
happens-before 於B中的任意操做ThreadB.join()
而且成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()
操做成功返回。interrupt()
方法的調用先行發生於被中斷線程代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()
方法檢測是否有中斷髮生finalize()
方法的開始第1條程序順序規則在一個線程中,全部的操做都是有序的,但實際上只要JMM的執行結果容許從新排序,這也是發生的重點——單線程執行結果是正確的,可是也不能保證多線程。
規則2,規則監視器的規則,很是好理解。在鎖被添加以前,鎖已經被釋放,而後它才能繼續被鎖定。
第三條規則適用於討論的不穩定性。若是一個行程序編寫一個變量,另外一個線程讀取它,那麼在操做以前必須讀取寫入操做。
第四個規則正在發生。
接下來的幾行不會重複。
從新引入volatile變量規則是很重要的:對於一個不穩定域的寫,出現以前,而後是對這個volatile字段的讀取。本文進行再次說,若是一個變量聲明事實上是不穩定,因此當我讀了變量,最新的價值老是能夠閱讀它,這個最新的值意味着不管什麼其餘線程寫操做,該變量將當即更新到主內存,我也能夠從主內存讀取只寫值。也就是說,volatile關鍵字保證了可視性和有序度。
繼續上面的代碼示例:
int a = 0; bool flag = false; public void write() { a = 2; //1 flag = true; //2 } public void multiply() { if (flag) { //3 int ret = a * a;//4 } }
當編寫一個volatile變量時,JMM在本地內存中刷新與主內存對應的本地內存中的共享變量。
當您讀取一個volatile變量時,JMM將使線程對應的本地內存失效,而後線程將從主內存讀取共享變量。