歡迎關注個人公眾號:石杉的架構筆記(ID:shishan100)
週一至週五早8點半!精品技術文章準時送上!
一、寫在前面
前段時間把幾年前帶過的一個專案架構演進的過程整理了一個系列出來,參見(億級流量架構系列專欄總結)。
不過很多同學看了之後,後臺反饋說文章太燒腦,看的雲裡霧裡。其實這個也正常,文章承載的資訊畢竟有限,而架構的東西細節太多,想要僅僅通過文章看懂一個系統架構的設計和落地,確實難度不小。
所以接下來用大白話跟大家聊點輕鬆的話題,比較易於理解,而且對大家工作和麵試都很有幫助。
二、場景引入,問題初現
很多同學出去面試,都會被問到一個常見的問題:說說你對volatile的理解?
不少初出茅廬的同學可能會有點措手不及,因為可能就是之前沒關注過這個。但是網上百度一下呢,不少文章寫的很好,但是理論扎的太深,文字太多,圖太少,讓人有點難以理解。
基於上述痛點,這篇文章嘗試站在年輕同學的角度,用最簡單的大白話,加上多張圖給大家說一下,volatile到底是什麼?
當然本文不會把理論扎的太深,因為一下子扎深了文字太多,很多同學還是會不好理解。
本文僅僅是定位在用大白話的語言將volatile這個東西解釋清楚,而涉及到特別底層的一些原理和技術問題,以後有機會開文再寫。
首先,給大家上一張圖,我們們來一起看看:
如上圖,這張圖說的是java記憶體模型中,每個執行緒有自己的工作記憶體,同時還有一個共享的主記憶體。
舉個例子,比如說有兩個執行緒,他們的程式碼裡都需要讀取data這個變數的值,那麼他們都會從主記憶體里載入data變數的值到自己的工作記憶體,然後才可以使用那個值。
好了,現在大家從圖裡看到,每個執行緒都把data這個變數的副本載入到了自己的工作記憶體裡了,所以每個執行緒都可以讀到data = 0這個值。
這樣,線上程程式碼執行的過程中,對data的值都可以直接從工作記憶體里載入了,不需要再從主記憶體里載入了。
那問題來了,為啥一定要讓每個執行緒用一個工作記憶體來存放變數的副本以供讀取呢?我直接讓執行緒每次都從主記憶體載入變數的值不行嗎?
很簡單!因為執行緒執行的程式碼對應的是一些指令,是由CPU執行的!但是CPU每次執行指令運算的時候,也就是執行我們寫的那一大坨程式碼的時候,要是每次需要一個變數的值,都從主記憶體載入,效能會比較差!
所以說後來想了一個辦法,就是執行緒有工作記憶體的概念,類似於一個高速的本地快取。
這樣一來,執行緒的程式碼在執行過程中,就可以直接從自己本地快取里載入變數副本,不需要從主記憶體載入變數值,效能可以提升很多!
但是大家思考一下,這樣會有什麼問題?
我們來設想一下,假如說執行緒1修改了data變數的值為1,然後將這個修改寫入自己的本地工作記憶體。那麼此時,執行緒1的工作記憶體裡的data值為1。
然而,主記憶體裡的data值還是為0!執行緒2的工作記憶體裡的data值還是0啊?!
這可尷尬了,那接下來,線上程1的程式碼執行過程中,他可以直接讀到data最新的值是1,但是執行緒2的程式碼執行過程中讀到的data的值還是0!
這就導致,執行緒1和執行緒2其實都是在操作一個變數data,但是執行緒1修改了data變數的值之後,執行緒2是看不到的,一直都是看到自己本地工作記憶體中的一箇舊的副本的值!
這就是所謂的java併發程式設計中的可見性問題:
多個執行緒併發讀寫一個共享變數的時候,有可能某個執行緒修改了變數的值,但是其他執行緒看不到!也就是對其他執行緒不可見!
三、volatile的作用及背後的原理
那如果要解決這個問題怎麼辦呢?這時就輪到volatile閃亮登場了!你只要給data這個變數在定義的時候加一個volatile,就直接可以完美的解決這個可見性的問題。
比如下面的這樣的程式碼,在加了volatile之後,會有啥作用呢?
完整的作用就不給大家解釋了,因為我們定位就是大白話,要是把底層涉及的各種記憶體屏障、指令重排等概念在這裡帶出來,不少同學又要蒙圈了!
我們這裡,就說說他最關鍵的幾個作用是啥?
第一,一旦data變數定義的時候前面加了volatile來修飾的話,那麼執行緒1只要修改data變數的值,就會在修改完自己本地工作記憶體的data變數值之後,強制將這個data變數最新的值刷回主記憶體,必須讓主記憶體裡的data變數值立馬變成最新的值!
整個過程,如下圖所示:
第二,如果此時別的執行緒的工作記憶體中有這個data變數的本地快取,也就是一個變數副本的話,那麼會強制讓其他執行緒的工作記憶體中的data變數快取直接失效過期,不允許再次讀取和使用了!
整個過程,如下圖所示:
第三,如果執行緒2在程式碼執行過程中再次需要讀取data變數的值,此時嘗試從本地工作記憶體中讀取,就會發現這個data = 0已經過期了!
此時,他就必須重新從主記憶體中載入data變數最新的值!那麼不就可以讀取到data = 1這個最新的值了!整個過程,參見下圖:
bingo!好了,volatile完美解決了java併發中可見性的問題!
對一個變數加了volatile關鍵字修飾之後,只要一個執行緒修改了這個變數的值,立馬強制刷回主記憶體。
接著強制過期其他執行緒的本地工作記憶體中的快取,最後其他執行緒讀取變數值的時候,強制重新從主記憶體來載入最新的值!
這樣就保證,任何一個執行緒修改了變數值,其他執行緒立馬就可以看見了!這就是所謂的volatile保證了可見性的工作原理!
四、總結 & 提醒
最後給大家提一嘴,volatile主要作用是保證可見性以及有序性。
有序性涉及到較為複雜的指令重排、記憶體屏障等概念,本文沒提及,但是volatile是不能保證原子性的!
也就是說,volatile主要解決的是一個執行緒修改變數值之後,其他執行緒立馬可以讀到最新的值,是解決這個問題的,也就是可見性!
但是如果是多個執行緒同時修改一個變數的值,那還是可能出現多執行緒併發的安全問題,導致資料值修改錯亂,volatile是不負責解決這個問題的,也就是不負責解決原子性問題!
原子性問題,得依賴synchronized、ReentrantLock等加鎖機制來解決。
END
如有收穫,請幫忙轉發,您的鼓勵是作者最大的動力,謝謝!
一大波微服務、分散式、高併發、高可用的原創系列文章正在路上
歡迎掃描下方二維碼,持續關注:
石杉的架構筆記(id:shishan100)
十餘年BAT架構經驗傾囊相授