关键字_Java Volatile关键字
作者| Arun Manivannan
譯者|有孚
編輯|包包 ? ??
Java的volatile關鍵字用于標記一個變量“應當存儲在主存”。更確切地說,每次讀取volatile變量,都應該從主存讀取,而不是從CPU緩存讀取。每次寫入一個volatile變量,應該寫到主存中,而不是僅僅寫到CPU緩存。
實際上,從Java 5開始,volatile關鍵字除了保證volatile變量從主存讀寫外,還提供了更多的保障。我將在后面的章節中進行說明。
變量可見性問題
Java的volatile關鍵字能保證變量修改后,對各個線程是可見的。這個聽起來有些抽象,下面就詳細說明。
在一個多線程的應用中,線程在操作非volatile變量時,出于性能考慮,每個線程可能會將變量從主存拷貝到CPU緩存中。如果你的計算機有多個CPU,每個線程可能會在不同的CPU中運行。這意味著,每個線程都有可能會把變量拷貝到各自CPU的緩存中,如下圖所示:
對于非volatile變量,JVM并不保證會從主存中讀取數據到CPU緩存,或者將CPU緩存中的數據寫到主存中。這會引起一些問題,在后面的章節中,我會來解釋這些問題。
試想一下,如果有兩個以上的線程訪問一個共享對象,這個共享對象包含一個counter變量,下面是代碼示例:
public class SharedObject { public int counter = 0;}如果只有線程1修改了(自增)counter變量,而線程1和線程2兩個線程都會在某些時刻讀取counter變量。
如果counter變量沒有聲明成volatile,則counter的值不保證會從CPU緩存寫回到主存中。也就是說,CPU緩存和主存中的counter變量值并不一致,如下圖所示:
這就是“可見性”問題,線程看不到變量最新的值,因為其他線程還沒有將變量值從CPU緩存寫回到主存。一個線程中的修改對另外的線程是不可見的。
volatile可見性保證
Java的volatile關鍵字就是設計用來解決變量可見性問題。將counter變量聲明為volatile,則在寫入counter變量時,也會同時將變量值寫入到主存中。同樣的,在讀取counter變量值時,也會直接從主存中讀取。
下面的代碼演示了如果將counter聲明為volatile:
public class SharedObject { public volatile int counter = 0;}將一個變量聲明為volatile,可以保證變量寫入時對其他線程的可見。
在上面的場景中,一個線程(T1)修改了counter,另一個線程(T2)讀取了counter(但沒有修改它),將counter變量聲明為volatile,就能保證寫入counter變量后,對T2是可見的。
然而,如果T1和T2都修改了counter的值,只是將counter聲明為volatile還遠遠不夠,后面會有更多的說明。
完整的volatile可見性保證
實際上,volatile的可見性保證并不是只對于volatile變量本身那么簡單。可見性保證遵循以下規則:
如果線程A寫入一個volatile變量,線程B隨后讀取了同樣的volatile變量,則線程A在寫入volatile變量之前的所有可見的變量值,在線程B讀取volatile變量后也同樣是可見的。
如果線程A讀取一個volatile變量,那么線程A中所有可見的變量也會同樣從主存重新讀取。
下面用一段代碼來示例說明:
public class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}update()方法寫入3個變量,其中只有days變量是volatile。
完整的volatile可見性保證意味著,在寫入days變量時,線程中所有可見變量也會寫入到主存。也就是說,寫入days變量時,years和months也會同時被寫入到主存。
下面的代碼讀取了years、months、days變量:
public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}請注意totalDays()方法開始讀取days變量值到total變量。在讀取days變量值時,months和years的值也會同時從主存讀取。因此,按上面所示的順序讀取時,可以保證讀取到days、months、years變量的最新值。
'' ? ?譯者注:可以將對volatile變量的讀寫理解為一個觸發刷新的操作,寫入volatile變量時,線程中的所有變量也都會觸發寫入主存。而讀取volatile變量時,也同樣會觸發線程中所有變量從主存中重新讀取。因此,應當盡量將volatile的寫入操作放在最后,而將volatile的讀取放在最前,這樣就能連帶將其他變量也進行刷新。上面的例子中,update()方法對days的賦值就是放在years、months之后,就是保證years、months也能將最新的值寫入到主存,如果是放在兩個變量之前,則days會寫入主存,而years、months則不會。反過來,totalDays()方法則將days的讀取放在最前面,就是為了能同時觸發刷新years、months變量值,如果是放后面,則years、months就可能還是從CPU緩存中讀取值,而不是從主存中獲取最新值。 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'' |
指令重排問題
出于性能考慮,JVM和CPU是允許對程序中的指令進行重排的,只要保證(重排后的)指令語義一致即可。如下代碼為例:
int a = 1;int b = 2;a++;b++;這些指令可以按以下順序重排,而不改變程序的語義:
int a = 1;a++;int b = 2;b++;然而,指令重排面臨的一個問題就是對volatile變量的處理。還是以前面提到的MyClass類來說明:
public class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}一旦update()變量寫了days值,則years、months的最新值也會寫入到主存。但是,如果JVM重排了指令,比如按以下方式重排:
public void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years;}在days被修改時,months、years的值也會寫入到主存,但這時進行寫入,months、years并不是新的值(譯者注:即在months、years被賦新值之前,就觸發了這兩個變量值寫入主存的操作,自然這兩個變量在主存中的值就不是新值)。新的值自然對其他線程是不可見的。指令重排導致了程序語義的改變。
Java對此有一個解決方法,我們會在下一章節中說明。
Java volatile Happens-Before保證
為了解決指令重排的問題,Java的volatile關鍵字在可見性之外,又提供了happends-before保證。happens-before原則如下:
如果有讀寫操作發生在寫入volatile變量之前,讀寫其他變量的指令不能重排到寫入volatile變量之后。寫入一個volatile變量之前的讀寫操作,對volatile變量是有happens-before保證的。注意,如果是寫入volatile之后,有讀寫其他變量的操作,那么這些操作指令是有可能被重排到寫入volatile操作指令之前的。但反之則不成立。即可以把位于寫入volatile操作指令之后的其他指令移到寫入volatile操作指令之前,而不能把位于寫入volatile操作指令之前的其他指令移到寫入volatile操作指令之后。
如果有讀寫操作發生在讀取volatile變量之后,讀寫其他變量的指令不能重排到讀取volatile變量之前。注意,如果是讀取volatile之前,有讀取其他變量的操作,那么這些操作指令是有可能被重排到讀取volatile操作指令之后的。但反之則不成立。即可以把位于讀取volatile操作指令之前的指令移到讀取volatile操作指令之后,而不能把位于讀取volatile操作指令之后的指令移到讀取volatile操作指令之前。
以上的happens-before原則為volatile關鍵字的可見性提供了強制保證。
'' ? ?譯者注:這兩個原則讀起來有些拗口(當然翻譯也不足夠好),其實就是不管JVM怎么去禁止/允許某些情況下的指令重排,最終就是保證“完整的volatile可見性保證”的那種效果,所以,只要理解了“完整的volatile可見性保證”的效果就足夠了 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? ? ? ? ?? '' |
volatile并不總是可行的
雖然volatile關鍵字能保證volatile變量的所有讀取都是直接從主存讀取,所有寫入都是直接寫入到主存中,但在一些情形下,僅僅是將變量聲明為volatile還是遠遠不夠的。
就像前面示例所說的,線程1寫入共享變量counter的值,將counter聲明為volatile已經足夠保證線程2總是能獲取到最新的值。
事實上,多個線程都能寫入共享的volatile變量,主存中也能存儲正確的變量值,然而這有一個前提,變量新值的寫入不能依賴于變量的舊值。換句話說,就是一個線程寫入一個共享volatile變量值時,不需要先讀取變量值,然后以此來計算出新的值。
如果線程需要先讀取一個volatile變量的值,以此來計算出一個新的值,那么volatile變量就不足夠保證正確的可見性。(線程間)讀寫volatile變量的時間間隔很短,這將導致一個競態條件,多個線程同時讀取了volatile變量相同的值,然后以此計算出了新的值,這時各個線程往主存中寫回值,則會互相覆蓋。
多個線程對counter變量進行自增操作就是這樣的情形,下面的章節會詳細說明這種情形。
設想一下,如果線程1將共享變量counter的值0讀取到它的CPU緩存,然后自增為1,而還沒有將新值寫回到主存。線程2這時從主存中讀取的counter值依然是0,依然放到它自身的CPU緩存中,然后同樣將counter值自增為1,同樣也還沒有將新值寫回到主存。如下圖所示:
從實際的情況來看,線程1和線程2現在就是不同步的。共享變量counter正確的值應該是2,但各個線程中CPU緩存的值都是1,而主存中的值依然是0。這是很混亂的。即使線程最終將共享變量counter的值寫回到主存,那值也明顯是錯的。
何時使用volatile
正如我前面所說,如果兩個線程同時讀寫一個共享變量,僅僅使用volatile關鍵字是不夠的。你應該使用 synchronized 來保證讀寫變量是原子的。(一個線程)讀寫volatile變量時,不會阻塞(其他)線程進行讀寫。你必須在關鍵的地方使用synchronized關鍵字來解決這個問題。
除了synchronized方法,你還可以使用java.util.concurrent包提供的許多原子數據類型來解決這個問題。比如,AtomicLong或AtomicReference,或是其他的類。
如果只有一個線程對volatile進行讀寫,而其他線程只是讀取變量,這時,對于只是讀取變量的線程來說,volatile就已經可以保證讀取到的是變量的最新值。如果沒有把變量聲明為volatile,這就無法保證。
volatile關鍵字對32位和64位的變量都有效。
volatile的性能考量
讀寫volatile變量會導致變量從主存讀寫。從主存讀寫比從CPU緩存讀寫更加“昂貴”。訪問一個volatile變量同樣會禁止指令重排,而指令重排是一種提升性能的技術。因此,你應當只在需要保證變量可見性的情況下,才使用volatile變量。
- - -END?- - -喜歡本文的朋友,歡迎關注公眾號??并發編程網,收看更多精彩內容
總結
以上是生活随笔為你收集整理的关键字_Java Volatile关键字的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么打开北信源加密u盘_全国首个!北信源
- 下一篇: java日志模块_Java源码初探_lo