volatile的作用是什么?
volatile
是一個輕量級的synchronized
,一般作用于 變量 ,在多處理器開發(fā)的過程中保證了內(nèi)存的可見性。相比于synchronized
關(guān)鍵字,volatile
關(guān)鍵字的執(zhí)行成本更低,效率更高。
volatile的特性有哪些?
并發(fā)編程的三大特性為可見性、有序性和原子性。通常來講
volatile
可以保證可見性和有序性。
- 可見性:
volatile
可以保證不同線程對共享變量進行操作時的可見性。即當一個線程修改了共享變量時,另一個線程可以讀取到共享變量被修改后的值。 - 有序性:
volatile
會通過禁止指令重排序進而保證有序性。 - 原子性:對于單個的
volatile
修飾的變量的讀寫是可以保證原子性的,但對于i++
這種復合操作并不能保證原子性。這句話的意思基本上就是說volatile
不具備原子性了。
Java內(nèi)存的可見性問題
Java的內(nèi)存模型如下圖所示。
這里的本地內(nèi)存并不是真實存在的,只是Java內(nèi)存模型的一個抽象概念,它包含了控制器、運算器、緩存等。同時Java內(nèi)存模型規(guī)定,線程對共享變量的操作必須在自己的本地內(nèi)存中進行,不能直接在主內(nèi)存中操作共享變量。這種內(nèi)存模型會出現(xiàn)什么問題呢?,
- 線程A獲取到共享變量X的值,此時本地內(nèi)存A中沒有X的值,所以加載主內(nèi)存中的X值并緩存到本地內(nèi)存A中,線程A修改X的值為1,并將X的值刷新到主內(nèi)存中,這時主內(nèi)存及本地內(nèi)存A中的X的值都為1。
- 線程B需要獲取共享變量X的值,此時本地內(nèi)存B中沒有X的值,加載主內(nèi)存中的X值并緩存到本地內(nèi)存B中,此時X的值為1。線程B修改X的值為2,并刷新到主內(nèi)存中,此時主內(nèi)存及本地內(nèi)存B中的X值為2,本地內(nèi)存A中的X值為1。
- 線程A再次獲取共享變量X的值,此時本地內(nèi)存中存在X的值,所以直接從本地內(nèi)存中A獲取到了X為1的值,但此時主內(nèi)存中X的值為2,到此出現(xiàn)了所謂內(nèi)存不可見的問題。
該問題Java內(nèi)存模型是通過synchronized
關(guān)鍵字和volatile
關(guān)鍵字就可以解決。
為什么代碼會重排序?
計算機在執(zhí)行程序的過程中,編譯器和處理器通常會對指令進行重排序,這樣做的目的是為了提高性能。具體可以看下面這個例子。
int a = 1;
int b = 2;
int a1 = a;
int b1 = b;
int a2 = a + a;
int b2 = b + b;
......
像這段代碼,不斷地交替讀取a和b,會導致寄存器頻繁交替存儲a和b,使得代碼性能下降,可對其進入如下重排序。
int a = 1;
int b = 2;
int a1 = a;
int a2 = a + a;
int b1 = b;
int b2 = b + b;
......
按照這樣的順序執(zhí)行代碼便可以避免交替讀取a和b,這就是重排序的意義。
指令重排序一般分為編譯器優(yōu)化重排、指令并行重排和內(nèi)存系統(tǒng)重排三種。
- 編譯器優(yōu)化重排:編譯器在不改變單線程程序語義的情況下,可以對語句的執(zhí)行順序進行重新排序。
- 指令并行重排:現(xiàn)代處理器多采用指令級并行技術(shù)來將多條指令重疊執(zhí)行。對于不存在數(shù)據(jù)依賴的程序,處理器可以對機器指令的執(zhí)行順序進行重新排列。
- 內(nèi)存系統(tǒng)重排:因為處理器使用緩存和讀/寫緩沖區(qū),使得加載(load)和存儲(store)看上去像是在亂序執(zhí)行。
注:簡單解釋下數(shù)據(jù)依賴性:如果兩個操作訪問了同一個變量,并且這兩個操作有一個是寫操作,這兩個操作之間就會存在數(shù)據(jù)依賴性,例如:
a = 1;
b = a;
如果對這兩個操作的執(zhí)行順序進行重排序的話,那么結(jié)果就會出現(xiàn)問題。
其實,這三種指令重排說明了一個問題,就是指令重排在單線程下可以提高代碼的性能,但在多線程下可以會出現(xiàn)一些問題。
重排序會引發(fā)什么問題?
前面已經(jīng)說過了,在單線程程序中,重排序并不會影響程序的運行結(jié)果,而在多線程場景下就不一定了。可以看下面這個經(jīng)典的例子,該示例出自《Java并發(fā)編程的藝術(shù)》。
class ReorderExample{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 操作1
flag = true; // 操作2
}
public void reader(){
if(flag){ // 操作3
int i = a + a; // 操作4
}
}
}
假設(shè)線程1先執(zhí)行writer()
方法,隨后線程2執(zhí)行reader()
方法,最后程序一定會得到正確的結(jié)果嗎?
答案是不一定的,如果代碼按照下圖的執(zhí)行順序執(zhí)行代碼則會出現(xiàn)問題。
操作1和操作2進行了重排序,線程1先執(zhí)行flag=true
,然后線程2執(zhí)行操作3和操作4,線程2執(zhí)行操作4時不能正確讀取到a
的值,導致最終程序運行結(jié)果出問題。這也說明了在多線程代碼中,重排序會破壞多線程程序的語義。
as-if-serial規(guī)則和happens-before規(guī)則的區(qū)別?
區(qū)別:
- as-if-serial定義:無論編譯器和處理器如何進行重排序,單線程程序的執(zhí)行結(jié)果不會改變。
- happens-before定義:一個操作happens-before另一個操作,表示第一個的操作結(jié)果對第二個操作可見,并且第一個操作的執(zhí)行順序也在第二個操作之前。但這并不意味著Java虛擬機必須按照這個順序來執(zhí)行程序。如果重排序的后的執(zhí)行結(jié)果與按happens-before關(guān)系執(zhí)行的結(jié)果一致,Java虛擬機也會允許重排序的發(fā)生。
- happens-before關(guān)系保證了同步的多線程程序的執(zhí)行結(jié)果不被改變,as-if-serial保證了單線程內(nèi)程序的執(zhí)行結(jié)果不被改變。
相同點:happens-before和as-if-serial的作用都是在不改變程序執(zhí)行結(jié)果的前提下,提高程序執(zhí)行的并行度。
voliatile的實現(xiàn)原理?
前面已經(jīng)講述
volatile
具備可見性和有序性兩大特性,所以volatile
的實現(xiàn)原理也是圍繞如何實現(xiàn)可見性和有序性展開的。
volatile實現(xiàn)內(nèi)存可見性原理
導致內(nèi)存不可見的主要原因就是Java內(nèi)存模型中的本地內(nèi)存和主內(nèi)存之間的值不一致所導致,例如上面所說線程A訪問自己本地內(nèi)存A的X值時,但此時主內(nèi)存的X值已經(jīng)被線程B所修改,所以線程A所訪問到的值是一個臟數(shù)據(jù)。那如何解決這種問題呢?
volatile
可以保證內(nèi)存可見性的關(guān)鍵是volatile
的讀/寫實現(xiàn)了緩存一致性,緩存一致性的主要內(nèi)容為:
- 每個處理器會通過嗅探總線上的數(shù)據(jù)來查看自己的數(shù)據(jù)是否過期,一旦處理器發(fā)現(xiàn)自己緩存對應的內(nèi)存地址被修改,就會將當前處理器的緩存設(shè)為無效狀態(tài)。此時,如果處理器需要獲取這個數(shù)據(jù)需重新從主內(nèi)存將其讀取到本地內(nèi)存。
- 當處理器寫數(shù)據(jù)時,如果發(fā)現(xiàn)操作的是共享變量,會通知其他處理器將該變量的緩存設(shè)為無效狀態(tài)。
那緩存一致性是如何實現(xiàn)的呢?可以發(fā)現(xiàn)通過volatile
修飾的變量,生成匯編指令時會比普通的變量多出一個Lock指令,這個Lock指令就是volatile
關(guān)鍵字可以保證內(nèi)存可見性的關(guān)鍵,它主要有兩個作用:
- 將當前處理器緩存的數(shù)據(jù)刷新到主內(nèi)存。
- 刷新到主內(nèi)存時會使得其他處理器緩存的該內(nèi)存地址的數(shù)據(jù)無效。
volatile實現(xiàn)有序性原理
前面提到重排序可以提高代碼的執(zhí)行效率,但在多線程程序中可以導致程序的運行結(jié)果不正確,那
volatile
是如何解決這一問題的呢?
為了實現(xiàn)volatile
的內(nèi)存語義,編譯器在生成字節(jié)碼時會通過插入內(nèi)存屏障來禁止指令重排序。
內(nèi)存屏障:內(nèi)存屏障是一種CPU指令,它的作用是對該指令前和指令后的一些操作產(chǎn)生一定的約束,保證一些操作按順序執(zhí)行。
Java虛擬機插入內(nèi)存屏障的策略
Java內(nèi)存模型把內(nèi)存屏障分為4類,如下表所示:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 保證Load1數(shù)據(jù)的讀取先于Load2及后續(xù)所有讀取指令的執(zhí)行 |
StoreStore Barriers | Store1;StoreStore;Store2 | 保證Store1數(shù)據(jù)刷新到主內(nèi)存先于Store2及后續(xù)所有存儲指令 |
LoadStore Barriers | Load1;LoadStore;Store2 | 保證Load1數(shù)據(jù)的讀取先于Store2及后續(xù)的所有存儲指令刷新到主內(nèi)存 |
StoreLoad Barriers | Store1;StoreLoad;Load2 | 保證Store1數(shù)據(jù)刷新到主內(nèi)存先于Load2及后續(xù)所有讀取指令的執(zhí)行 |
注:StoreLoad Barriers同時具備其他三個屏障的作用,它會使得該屏障之前的所有內(nèi)存訪問指令完成之后,才會執(zhí)行該屏障之后的內(nèi)存訪問命令。
Java內(nèi)存模型對編譯器指定的volatile
重排序規(guī)則為:
- 當?shù)谝粋€操作是
volatile
讀時,無論第二個操作是什么都不能進行重排序。 - 當?shù)诙€操作是
volatile
寫時,無論第一個操作是什么都不能進行重排序。 - 當?shù)谝粋€操作是
volatile
寫,第二個操作為volatile
讀時,不能進行重排序。
根據(jù)volatile
重排序規(guī)則,Java內(nèi)存模型采取的是保守的屏障插入策略,volatile
寫是在前面和后面分別插入內(nèi)存屏障,volatile
讀是在后面插入兩個內(nèi)存屏障,具體如下:
volatile
讀:在每個volatile
讀后面分別插入LoadLoad屏障及LoadStore屏障(根據(jù)volatile重排序規(guī)則第一條),如下圖所示
LoadLoad屏障的作用:禁止上面的所有普通讀操作和上面的volatile
讀操作進行重排序。
LoadStore屏障的作用:禁止下面的普通寫和上面的volatile
讀進行重排序。
volatile
寫:在每個volatile
寫前面插入一個StoreStore屏障(為滿足volatile
重排序規(guī)則第二條),在每個volatile
寫后面插入一個StoreLoad屏障(為滿足voaltile
重排序規(guī)則第三條),如下圖所示
StoreStore屏障的作用:禁止上面的普通寫和下面的volatile
寫重排序
StoreLoad屏障的作用:防止上面的volatile
寫與下面可能出現(xiàn)的volatile
讀/寫重排序。
編譯器對內(nèi)存屏障插入策略的優(yōu)化
因為Java內(nèi)存模型所采用的屏障插入策略比較保守,所以在實際的執(zhí)行過程中,只要不改變
volatile
讀/寫的內(nèi)存語義,編譯器通常會省略一些不必要的內(nèi)存屏障。
代碼如下:
public class VolatileBarrierDemo{
int a;
volatile int b = 1;
volatile int c = 2;
public void test(){
int i = b; //volatile讀
int j = c; //volatile讀
a = i + j; //普通寫
}
}
指令序列示意圖如下:
從上圖可以看出,通過指令優(yōu)化一共省略了兩個內(nèi)存屏障(虛線表示),省略第一個內(nèi)存屏障LoadStore的原因是最后的普通寫不可能越過第二個volatile
讀,省略第二個內(nèi)存屏障LoadLoad的原因是下面沒有涉及到普通讀的操作。
volatile能使一個非原子操作變成一個原子操作嗎?
volatile
只能保證可見性和有序性,但可以保證64位的long
型和double
型變量的原子性。
對于32位的虛擬機來說,每次原子讀寫都是32位的,會將long
和double
型變量拆分成兩個32位的操作來執(zhí)行,這樣long
和double
型變量的讀寫就不能保證原子性了,而通過volatile
修飾的long
和double
型變量則可以保證其原子性。
volatile、synchronized的區(qū)別?
volatile
主要是保證內(nèi)存的可見性,即變量在寄存器中的內(nèi)存是不確定的,需要從主存中讀取。synchronized
主要是解決多個線程訪問資源的同步性。volatile
作用于變量,synchronized
作用于代碼塊或者方法。volatile
僅可以保證數(shù)據(jù)的可見性,不能保證數(shù)據(jù)的原子性。synchronized
可以保證數(shù)據(jù)的可見性和原子性。volatile
不會造成線程的阻塞,synchronized
會造成線程的阻塞。
-
處理器
+關(guān)注
關(guān)注
68文章
18924瀏覽量
227199 -
開發(fā)
+關(guān)注
關(guān)注
0文章
357瀏覽量
40734 -
volatile
+關(guān)注
關(guān)注
0文章
44瀏覽量
12979
發(fā)布評論請先 登錄
相關(guān)推薦
評論