做者 某人Valar
如需轉載請保留原文連接html部分圖片來自百度,若有侵權請聯繫刪除java
相關推薦:c++
Java多線程之volatilegithub
目錄:面試
volatile中文意爲揮發物,不穩定的。在Java中也是一個關鍵字,用於修飾變量。算法
在JMM(Java Memory Model,Java內存模型)中,有main memory,每一個線程也有本身的memory (例如寄存器)。爲了性能,一個線程會在本身的memory中保持要訪問的變量的副本。緩存
這樣就會出現同一個變量在某個瞬間,在一個線程的memory中的值可能與另一個線程memory中的值,或者main memory中的值不一致的狀況。安全
一個變量聲明爲volatile,就意味着這個變量是隨時會被其餘線程修改,線程在每次使用變量的時候,都會讀取變量修改後的最新值。bash
Java內存模型圖:
![]()
volatile
不管是修飾實例變量仍是靜態變量,都須要放在數據類型
關鍵字以前,即放在String
、int
等以前。volatile
和final
不能同時修飾一個變量。volatile 是保證變量被寫時其結果其餘線程可見,而final已經讓該變量不能被再次寫了。關於原子性、可見性和有序性的介紹,以前的一篇文章有了介紹,傳送門
不能。
例如咱們常碰到的i++的問題。
i = 1; //原子性操做,不用使用volatile也不會出現線程安全問題。
複製代碼
volatile int i = 0;
i++; //非原子性操做
複製代碼
若是咱們開啓200個線程併發執行i++
這行代碼,每一個線程中只執行一遍。若是volatile能夠保證原子性的話,那麼i的最終結果應該是200;而實際上咱們發現這個值是會小於200的,緣由是什麼呢?
// i++ 其能夠被拆解爲
一、線程讀取i
二、temp = i + 1
三、i = temp
複製代碼
temp = i + 1
的操做, 要注意,此時的 i 的值尚未變化,而後B線程也執行了temp = i + 1
的操做,注意,此時A,B兩個線程保存的 i 的值都是5,temp 的值都是6i = temp
(6)的操做,此時i的值會當即刷新到主存並通知其餘線程保存的 i 值失效, 此時B線程須要從新讀取 i 的值那麼此時B線程保存的 i 就是6i=temp
(6),因此致使了計算結果比預期少了1。那麼如何保證i++這種操做的線程安全呢?
synchronized
關鍵字或者Lock
。至於爲何,能夠看下synchronized與原子性synchronized(object){
i++;
}
複製代碼
java.util.concurrent.atomic.AtomicInteger
,它使用的是CAS(compare and swap,比較並替換)算法,效率優於第 1 種。volatile關鍵字的變量寫操做時,強制緩存和主存同步,其餘線程讀時候發現緩存失效,就去讀主存,由此保證了變量的可見性。
volatile能夠禁止指令重排序,因此說其是能夠保證有序性的。
什麼是指令重排序(Instruction Reorder)?
在Java內存模型中,容許編譯器和處理器對指令進行重排序,重排序的結果不會影響到單線程的執行,但不能保證多線程併發執行時不受影響。
例如如下代碼在未發生指令重排序時,其執行順序爲1->2->3->4。但在真正執行時,將可能變爲1->2->4->3或者2->1->3->4或者其餘。但其會保證1處於3以前,2處於4以前。全部最終結果都是
a=10; b=20
。int a = 0;//語句1 int b = 1;//語句2 a = 10; //語句3 b = 20; //語句4 複製代碼
但若是是多線程狀況下,另外一個線程中有如下程序。當上述的執行順序被重排序爲1->2->4->3,當線程1執行到第3步
b=20
時,切換到線程2執行,其會輸出a此時已是10了
,而此時a的值其實仍是爲0。if(b == 20){ System.out.print("a此時已是10了"); } 複製代碼
內存屏障
。內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,其是一種CPU指令,因此像Java、c++、c語言都有此概念。
A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a CPU or compiler to enforce an ordering constraint on memory operations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier. ——— 維基百科
//抽象場景:
Load1;
LoadLoad;
Load2
複製代碼
Load1 和 Load2 表明兩條讀取指令。在Load2要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。
//抽象場景:
Store1;
StoreStore;
Store2
複製代碼
Store1 和 Store2表明兩條寫入指令。在Store2寫入執行前,保證Store1的寫入操做對其它處理器可見
//抽象場景:
Load1;
LoadStore;
Store2
複製代碼
在Store2被寫入前,保證Load1要讀取的數據被讀取完畢。
//抽象場景:
Store1;
StoreLoad;
Load2
複製代碼
在Load2讀取操做執行前,保證Store1的寫入對全部處理器可見。StoreLoad屏障的開銷是四種屏障中最大的。
在一個變量被volatile修飾後,JVM會爲咱們作兩件事:
仍是使用上面的例子:
此次使用volatile修飾變量b
int a = 0;//語句1
volatile int b = 1;//語句2
//在線程1中執行的語句
a = 10; //語句3
b = 20; //語句4
//在線程2中執行的語句
if(b == 20){
System.out.print("a此時已是10了");
}
複製代碼
在編譯以後線程1中的語句將相似於
a = 10; //語句3
----------- StoreStore屏障 ---------------
b = 20; //語句4
----------- StoreLoad屏障 ---------------
複製代碼
因爲屏障的存在,語句3
和語句4
將沒法被指令重排序,從而能夠保證在b=20時,a已經被賦值爲10了。那麼這個程序也就不存在線程安全問題了。
內存屏障阻礙了CPU採用優化技術來下降內存操做延遲,必須考慮所以帶來的性能損失。爲了達到最佳性能,最好是把要解決的問題模塊化,這樣處理器能夠按單元執行任務,而後在任務單元的邊界放上全部須要的內存屏障。採用這個方法可讓處理器不受限的執行一個任務單元。
要知道volatile是如何保證可見性的須要先了解下有關CPU緩存的概念。
咱們知道CPU的運算速度要比內存的讀寫速度快不少,這就形成了內存沒法跟上CPU的狀況,由此出現了CPU緩存。其是CPU與內存之間的臨時數據交換器,咱們常見的CPU會有3級緩存,常稱爲L一、L二、L3。
下圖是Intel Core i7處理器的高速緩存概念模型(圖片來自《深刻理解計算機系統》)
當系統運行時,CPU執行計算的過程以下:
在上述的緩存模型下,當多核併發執行某項任務時就容易出現問題。eg.
爲了解決這類問題,出現了針對CPU的MESI協議。
在早期的CPU中,是經過在總線加LOCK#鎖的方式實現的(又稱總線鎖)。當一個CPU對其緩存中的數據進行操做的時候,往總線中發送一個Lock信號。 這個時候,全部CPU收到這個信號以後就不操做本身緩存中的對應數據了,當操做結束,釋放鎖之後,全部的CPU就去內存中獲取最新數據更新。
但這種方式開銷太大,因此Intel開發了緩存一致性協議,也就是MESI協議。它的方法是在CPU緩存中保存一個標記位,這個標記位有四種狀態:
CPU的讀取遵循下面幾點:
舉個常見的例子就是:
當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,那麼他會發出信號通知其餘CPU將該變量的緩存行設置爲無效狀態。當其餘CPU使用這個變量時,首先會去嗅探是否有對該變量更改的信號,當發現這個變量的緩存行已經無效時,會重新從內存中讀取這個變量。
瞭解了上面的內容,就能夠很容易的理解volatile是如何實現的了。
參考:
volatile到此也介紹的很多了,最後來講下其與synchronized的區別。
瞭解更多synchronized的相關內容,請戳這裏。
當你和麪試官說到這裏時,你最好清楚裏面的具體細節,例如是從何種角度來看的有序性,以及如何實現的該特性,否則面試官很容易被問住的。
至此關於volatile的內容到這裏就結束了,若是文中有錯誤的地方、或者有其餘關於volatile
比較重要的內容又沒有介紹到的,歡迎在評論區裏留言,一塊兒交流學習。