Java面試中經常會涉及關於volatile的問題。本文梳理下volatile關鍵知識點。
volatile字意為“易失性”,在Java中用做修飾物件變數。它不是Java特有,在C,C++,C#等程式語言也存在,只是在其它程式語言中使用有所差異,但總體語義一致。比如使用volatile 能阻止編譯器對變數的讀寫優化。簡單說,如果一個變數被修飾為volatile,相當於告訴系統說我容易變化,編譯器你不要隨便優化(重排序,快取)我。
Happens-before
規範上,Java記憶體模型遵行happens-before。
volatile變數在多執行緒中,寫執行緒和讀執行緒具有happens-before關係。也就是寫值的執行緒要在讀取執行緒之前,並且讀執行緒能完全看見寫執行緒的相關變數。
happens-before:如果兩個有兩個動作AB,A發生在B之前,那麼A的順序應該在B前面並且A的操作對B完全可見。
happens-before 具有傳遞性,如果A發生在B之前,而B發生在C之前,那麼A發生在C之前。
如何保證可見性
多執行緒環境下counter變數的更新過程。執行緒1先從主存拷貝副本到CPU快取,然後CPU執行counter=7,修改完後寫入CPU快取,等待時機同步到主存。線上程1同步主存前,執行緒2讀到counter值依然為0。此時已經發生記憶體一致性錯誤(對於相同的共享資料,多執行緒讀到檢視不一致)。因為執行緒2看不見執行緒1操作結果,也將這個問題稱為可見性問題。
public class SharedObject {
public int counter = 0;
}
因為多了快取優化導致,導致可見性問題。所以volatile通過消除快取(描述可能不太準確)來避免。例如當使用volatile修飾變數後,操作該變數讀寫直接與主存互動,跳過快取層,保證其它讀執行緒每次獲取的都是最新值。
public volatile int counter = 0;
volatile 不單隻消除修飾的變數的快取。事實上與之相關的變數在讀寫時也會消除快取,如同使用了volatile一樣。
如下 years,months,days 三個變數中只有days是volatile,但是對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;
}
}
這是為什麼?我們分析一下。
一個寫執行緒呼叫 update,讀執行緒呼叫totalDays。單執行緒中,對於update方法,wa與wb存在happens-before關係, wa
在 wb
之前執行並對wb
可見。
多執行緒中rc與wb存在happens-before關係,wb
在rc
之前執行並對rc
可見。根據 happens-before傳遞性,wa
需要在rc
前先執行並對rc
可見。
因為wb
是volatile變數,所以rc
獲取的years,months也是最新值。
我們知道出於效能原因,JVM和CPU會對程式中的指令進行重新排序。如果update方法裡面wa
和wb
順序被重排,那它們的happens-before關係將不在成立。
為了避免這個問題,volatile對重排序做了保證 對於發生在volatile變數操作前的其他變數的操作不能重新排序。
由此我們得到volatile通過消除快取和防止重排保證執行緒的可見性。
volatile保證執行緒安全?
討論執行緒安全,大家都會提及原子性,順序性,可見性。volatile側重於保證可見性,也就是當寫的執行緒更新後,讀執行緒總能獲得最新值。在只有一個執行緒寫,多個執行緒讀的場景下,volatile能滿足執行緒安全。可如果多個執行緒同時寫入volatile變數時,則需要引入同步語義才能保證執行緒安全。
模擬10個執行緒同時寫入volatile變數,一個執行緒讀counter,執行完後正確結果應該是counter=10。
public static class WriterTask implements Runnable {
private final ShareObject share;
private final CountDownLatch countDownLatch;
public WriterTask(ShareObject share, CountDownLatch countDownLatch) {
this.share = share;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
countDownLatch.countDown();
share.increase();
}
}
public class ShareObject {
private volatile int counter;
public void increase() {
this.counter++;
}
}
執行結果出現counter=5或6 錯誤結果。
通過 synchronized,Lock或AtomicInteger 原子變數保證了結果的正確。
完整demo https://gist.github.com/onlythinking/ba7ca7aa5faf00a58f4cedae474fa6f6
volatile效能
volatile變數帶來可見性的保證,訪問volatile變數還防止了指令重排序。不過這一切是以犧牲優化(消除快取,直接操作主存開銷增加)為代價,所以不應該濫用volatile,僅在確實需要增強變數可見性的時候使用。
總結
本文記錄了volatile變數通過消除快取,防止指令重排序來保證執行緒可見性,並且在多執行緒寫入的變數的場景下,不保證執行緒安全。
歡迎大家留言交流,一起學習分享!!!