本人免費整理了Java高級資料,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G,須要本身領取。
傳送門:https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Qjava
1. volatile簡介
在上一篇文章中咱們深刻理解了java關鍵字node
這篇文章帶你完全理解程序員
咱們知道在java中還有一大神器就是關鍵volatile,能夠說是和synchronized各領風騷,其中奧妙,咱們來共同探討下。編程
經過上一篇的文章咱們瞭解到synchronized是阻塞式同步,在線程競爭激烈的狀況下會升級爲重量級鎖。而volatile就能夠說是java虛擬機提供的最輕量級的同步機制。緩存
但它同時不容易被正確理解,也至於在併發編程中不少程序員遇到線程安全的問題就會使用synchronized。安全
Java內存模型告訴咱們,各個線程會將共享變量從主內存中拷貝到工做內存,而後執行引擎會基於工做內存中的數據進行操做處理。性能優化
線程在工做內存進行操做後什麼時候會寫到主內存中?這個時機對普通變量是沒有規定的,而針對volatile修飾的變量給java虛擬機特殊的約定,線程對volatile變量的修改會馬上被其餘線程所感知,即不會出現數據髒讀的現象,從而保證數據的「可見性」。併發
如今咱們有了一個大概的印象就是:被volatile修飾的變量可以保證每一個線程可以獲取該變量的最新值,從而避免出現數據髒讀的現象。app
2. volatile實現原理
volatile是怎樣實現了?好比一個很簡單的Java代碼:
instance = new Instancce() //instance是volatile變量
在生成彙編代碼時會在volatile修飾的共享變量進行寫操做的時候會多出Lock前綴的指令(具體的你們可使用一些工具去看一下,這裏我就只把結果說出來)。咱們想這個Lock指令確定有神奇的地方,那麼Lock前綴的指令在多核處理器下會發現什麼事情了?分佈式
主要有這兩個方面的影響:
1.將當前處理器緩存行的數據寫回系統內存;
2.這個寫回內存的操做會使得其餘CPU裏緩存了該內存地址的數據無效
爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。
若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。
可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。所以,通過分析咱們能夠得出以下結論:
這樣針對volatile變量經過這樣的機制就使得每一個線程都能得到該變量的最新值。
3. volatile的happens-before關係
通過上面的分析,咱們已經知道了volatile變量能夠經過緩存一致性協議保證每一個線程都能得到最新值,即知足數據的「可見性」。
咱們繼續延續上一篇分析問題的方式(我一直認爲思考問題的方式是屬於本身,也纔是最重要的,也在不斷培養這方面的能力),我一直將併發分析的切入點分爲兩個核心,三大性質。
兩大核心:JMM內存模型(主內存和工做內存)以及happens-before;三條性質:原子性,可見性,有序性(關於三大性質的總結在之後得文章會和你們共同探討)。
廢話很少說,先來看兩個核心之一:volatile的happens-before關係。
在六條happens-before規則中有一條是:**volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。**下面咱們結合具體的代碼,咱們利用這條規則推導下:
public class VolatileExample { private int a = 0; private volatile boolean flag = false; public void writer(){ a = 1; //1 flag = true; //2 } public void reader(){ if(flag){ //3 int i = a; //4 } } }
上面的實例代碼對應的happens-before關係以下圖所示:
加鎖線程A先執行writer方法,而後線程B執行reader方法圖中每個箭頭兩個節點就代碼一個happens-before關係,黑色的表明根據程序順序規則推導出來,紅色的是根據volatile變量的寫happens-before 於任意後續對volatile變量的讀,而藍色的就是根據傳遞性規則推導出來的。
這裏的2 happen-before 3,一樣根據happens-before規則定義:若是A happens-before B,則A的執行結果對B可見,而且A的執行順序先於B的執行順序,咱們能夠知道操做2執行結果對操做3來講是可見的,也就是說當線程A將volatile變量 flag更改成true後線程B就可以迅速感知。
4. volatile的內存語義
仍是按照兩個核心的分析方式,分析完happens-before關係後咱們如今就來進一步分析volatile的內存語義(按照這種方式去學習,會不會讓你們對知識可以把握的更深,而不至於不知所措,若是你們認同個人這種方式,不妨給個贊,小弟在此謝過,對我是個鼓勵)。
仍是以上面的代碼爲例,假設線程A先執行writer方法,線程B隨後執行reader方法,初始時線程的本地內存中flag和a都是初始狀態,下圖是線程A執行volatile寫後的狀態圖。
當volatile變量寫後,線程中本地內存中共享變量就會置爲失效的狀態,所以線程B再須要讀取從主內存中去讀取該變量的最新值。下圖就展現了線程B讀取同一個volatile變量的內存變化示意圖。
從橫向來看,線程A和線程B之間進行了一次通訊,線程A在寫volatile變量時,實際上就像是給B發送了一個消息告訴線程B你如今的值都是舊的了,而後線程B讀這個volatile變量時就像是接收了線程A剛剛發送的消息。既然是舊的了,那線程B該怎麼辦了?天然而然就只能去主內存去取啦。
好的,咱們如今兩個核心:happens-before以及內存語義如今已經都瞭解清楚了。是否是還不過癮,忽然發現原來本身會這麼愛學習(微笑臉),那咱們下面就再來一點乾貨----volatile內存語義的實現。
4.1 volatile的內存語義實現
咱們都知道,爲了性能優化,JMM在不改變正確語義的前提下,會容許編譯器和處理器對指令序列進行重排序,那若是想阻止重排序要怎麼辦了?答案是能夠添加內存屏障。
內存屏障
JMM內存屏障分爲四類見下圖,
java編譯器會在生成指令系列時在適當的位置會插入內存屏障指令來禁止特定類型的處理器重排序。
爲了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:
"NO"表示禁止重排序。爲了實現volatile內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
對於編譯器來講,發現一個最優佈置來最小化插入屏障的總數幾乎是不可能的,爲此,JMM採起了保守策略:
須要注意的是:volatile寫是在前面和後面分別插入內存屏障,而volatile讀操做是在後面插入兩個內存屏障
StoreStore屏障:禁止上面的普通寫和下面的volatile寫重排序;
StoreLoad屏障:防止上面的volatile寫與下面可能有的volatile讀/寫重排序
LoadLoad屏障:禁止下面全部的普通讀操做和上面的volatile讀重排序
LoadStore屏障:禁止下面全部的普通寫操做和上面的volatile讀重排序
下面以兩個示意圖進行理解,圖片摘自至關好的一本書《java併發編程的藝術》。
5. 一個示例
咱們如今已經理解volatile的精華了,文章開頭的那個問題我想如今咱們都能給出答案了。更正後的代碼爲:
public class VolatileDemo { private static volatile boolean isOver = false; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { while (!isOver) ; } }); thread.start(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } isOver = true; } }
注意不一樣點,如今已經將isOver設置成了volatile變量,這樣在main線程中將isOver改成了true後,thread的工做內存該變量值就會失效,從而須要再次從主內存中讀取該值,如今可以讀出isOver最新值爲true從而可以結束在thread裏的死循環,從而可以順利中止掉thread線程。