點贊再看,養成習慣,微信搜索【三太子敖丙】關注這個互聯網苟且偷生的工具人。java
本文 GitHub github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及個人系列文章。git
Volatile多是面試裏面必問的一個話題吧,對他的認知不少朋友也僅限於會用階段,今天咱們換個角度去看看。程序員
先來跟着丙丙來看一段demo的代碼github
你會發現,永遠都不會輸出有點東西這一段代碼,按道理線程改了flag變量,主線程也能訪問到的呀?web
爲會出現這個狀況呢?那咱們就須要聊一下另一個東西了。面試
JMM
:Java內存模型,是java虛擬機規範中所定義的一種內存模型,Java內存模型是標準化的,屏蔽掉了底層不一樣計算機的區別(注意這個跟JVM徹底不是一個東西,只有還有小夥伴搞錯的
)。編程
那正式聊以前,丙丙先大概科普一下現代計算機的內存模型吧。緩存
其實早期計算機中cpu和內存的速度是差很少的,但在現代計算機中,cpu的指令速度遠超內存的存取速度
,因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)
來做爲內存與處理器之間的緩衝。安全
將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。微信
基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來更高的複雜度,由於它引入了一個新的問題:緩存一致性(CacheCoherence)
。
在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(MainMemory)。
而後咱們能夠聊一下JMM了。
Java內存模型(JavaMemoryModel)
描述了Java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量,存儲到內存和從內存中讀取變量這樣的底層細節。
全部的共享變量都存儲於主內存,這裏所說的變量指的是實例變量和類變量,不包含局部變量,由於局部變量是線程私有的,所以不存在競爭問題。
每個線程還存在本身的工做內存,線程的工做內存,保留了被線程使用的變量的工做副本。
線程對變量的全部的操做(讀,取)都必須在工做內存中完成,而不能直接讀寫主內存中的變量
。
不一樣線程之間也不能直接訪問對方工做內存中的變量,線程間變量的值的傳遞須要經過主內存中轉來完成。
正是由於這樣的機制,才致使了可見性問題的存在,那咱們就討論下可見性的解決方案。
由於某一個線程進入synchronized代碼塊先後,線程會得到鎖,清空工做內存,從主內存拷貝共享變量最新的值到工做內存成爲副本,執行代碼,將修改後的副本的值刷新回主內存中,線程釋放鎖。
而獲取不到鎖的線程會阻塞等待,因此變量的值確定一直都是最新的。
開頭的代碼優化完以後應該是這樣的:
每一個線程操做數據的時候會把數據從主內存讀取到本身的工做內存,若是他操做了數據而且寫會了,他其餘已經讀取的線程的變量副本就會失效了,須要都數據進行操做又要再次去主內存中讀取了。
volatile保證不一樣線程對共享變量操做的可見性,也就是說一個線程修改了volatile修飾的變量,當修改寫回主內存時,另一個線程當即看到最新的值。
是否是看着加一個關鍵字很簡單,但實際上他在背後含辛茹苦默默付出了很多,我從計算機層面的緩存一致性協議解釋一下這些名詞的意義。
以前咱們說過當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致,舉例說明變量在多個CPU之間的共享。
若是真的發生這種狀況,那同步回到主內存時以誰的緩存數據爲準呢?
爲了解決一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有MSI、MESI(IllinoisProtocol)
、MOSI、Synapse、Firefly及DragonProtocol等。
聊一下Intel的MESI吧
當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。
每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。
因爲Volatile的MESI緩存一致性協議,須要不斷的從主內存嗅探和cas不斷循環,無效交互會致使總線帶寬達到峯值。
因此不要大量使用Volatile,至於何時去使用Volatile何時使用鎖,根據場景區分。
咱們再來聊一下指令重排序
的問題
爲了提升性能,編譯器和處理器經常會對既定的代碼執行順序進行指令重排序。
一個好的內存模型實際上會放鬆對處理器和編譯器規則的束縛,也就是說軟件技術和硬件技術都爲同一個目標,而進行奮鬥:在不改變程序執行結果的前提下,儘量提升執行效率。
JMM對底層儘可能減小約束,使其可以發揮自身優點。
所以,在執行程序時,爲了提升性能,編譯器和處理器經常會對指令進行重排序。
通常重排序能夠分爲以下三種:
編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序;
指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序;
內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行的。
這裏還得提一個概念,as-if-serial
。
無論怎麼重排序,單線程下的執行結果不能被改變。
編譯器、runtime和處理器都必須遵照as-if-serial語義。
java編譯器會在生成指令系列時在適當的位置會插入內存屏障
指令來禁止特定類型的處理器重排序。
爲了實現volatile的內存語義,JMM會限制特定類型的編譯器和處理器重排序,JMM會針對編譯器制定volatile重排序規則表:
須要注意的是:volatile寫是在前面和後面分別插入內存屏障,而volatile讀操做是在後面插入兩個內存屏障。
上面的我提太重排序原則,爲了提升處理速度,JVM會對代碼進行編譯優化,也就是指令重排序優化,併發編程下指令重排序會帶來一些安全隱患:如指令重排序致使的多個線程操做之間的不可見性。
若是讓程序員再去了解這些底層的實現以及具體規則,那麼程序員的負擔就過重了,嚴重影響了併發編程的效率。
從JDK5開始,提出了happens-before
的概念,經過這個概念來闡述操做之間的內存可見性。
若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。
volatile域規則:對一個volatile域的寫操做,happens-before於任意線程後續對這個volatile域的讀。
若是如今個人變了falg變成了false,那麼後面的那個操做,必定要知道我變了。
聊了這麼多,咱們要知道Volatile是沒辦法保證原子性的,必定要保證原子性,可使用其餘方法。
就是一次操做,要麼徹底成功,要麼徹底失敗。
假設如今有N個線程對同一個變量進行累加也是沒辦法保證結果是對的,由於讀寫這個過程並非原子性的。
要解決也簡單,要麼用原子類,好比AtomicInteger,要麼加鎖(記得關注Atomic的底層
)。
單例有8種寫法,我說一下里面比較特殊的一種,涉及Volatile的。
我先講一下禁止指令重排序
的好處。
對象實際上建立對象要進過以下幾個步驟:
上面我不是說了嘛,是可能發生指令重排序的,那有可能構造函數在對象初始化完成前就賦值完成了,在內存裏面開闢了一片存儲區域後直接返回內存的引用,這個時候還沒真正的初始化完對象。
可是別的線程去判斷instance!=null,直接拿去用了,其實這個對象是個半成品,那就有空指針異常了。
由於可見性,線程A在本身的內存初始化了對象,還沒來得及寫回主內存,B線程也這麼作了,那就建立了多個對象,不是真正意義上的單例了。
上面提到了volatile與synchronized,那我聊一下他們的區別。
volatile只能修飾實例變量和類變量,而synchronized能夠修飾方法,以及代碼塊。
volatile保證數據的可見性,可是不保證原子性(多線程進行寫操做,不保證線程安全);而synchronized是一種排他(互斥)的機制。 volatile用於禁止指令重排序:能夠解決單例雙重檢查對象初始化代碼執行亂序問題。
volatile能夠看作是輕量版的synchronized,volatile不保證原子性,可是若是是對一個共享變量進行多個線程的賦值,而沒有其餘的操做,那麼就能夠用volatile來代替synchronized,由於賦值自己是有原子性的,而volatile又保證了可見性,因此就能夠保證線程安全了。
注:以上全部的內容若是能所有掌握我想Volatile在面試官那是很加分了,可是我還沒講到不少關於計算機內存那一塊的底層,那你們就須要後面去補課了,若是等得及,也能夠等到我寫計算機基礎章節。
由於更新文章和視頻,丙丙已經半年多的週末沒休息了,都是在公司那個工位沖沖衝,一直想找時間出去玩,想着年假一天沒用,就請了兩天出去玩一下。
這樣五一就能夠早點回來,準備恢復視頻的更新,你在看的時候呢,敖丙應該在出遊的列車上了,是的我就背了這個包,到寫完的時候,我還沒肯定去哪裏,提早祝你們節日愉快。
我是敖丙,一個在互聯網苟且偷生的工具人。
你知道的越多,你不知道的越多,人才們的 【三連】 就是丙丙創做的最大動力,咱們下期見!
注:若是本篇博客有任何錯誤和建議,歡迎人才們留言,你快說句話啊!
文章持續更新,能夠微信搜索「 三太子敖丙 」第一時間閱讀,回覆【資料】【面試】【簡歷】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。