上一篇,我們談了談如何通過同步來保證共用變數的原子性(一個操作或者多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行),本篇我們來談一談如何保證共用變數的可見性(多個線程訪問同一個變數時,一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值)。 我們使用同步的目的不僅是,不希 ...
上一篇,我們談了談如何通過同步來保證共用變數的原子性(一個操作或者多個操作要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行),本篇我們來談一談如何保證共用變數的可見性(多個線程訪問同一個變數時,一個線程修改了這個變數的值,其他線程能夠立即看得到修改的值)。
我們使用同步的目的不僅是,不希望某個線程在使用對象狀態時,另外一個線程在修改狀態,這樣容易造成混亂;我們還希望某個線程修改了對象狀態後,其他線程能夠看到修改後的狀態——這就涉及到了一個新的名詞:記憶體(可省略)可見性。
要瞭解可見性,我們得先來瞭解一下 Java 記憶體模型。
Java 記憶體模型(Java Memory Model,簡稱 JMM)描述了 Java 程式中各種變數(線程之間的共用變數)的訪問規則,以及在 JVM 中將變數存儲到記憶體→從記憶體中讀取變數的底層細節。
要知道,所有的變數都是存儲在主記憶體中的,每個線程會有自己獨立的工作記憶體,裡面保存了該線程使用到的變數副本(主記憶體中變數的一個拷貝)。見下圖。
也就是說,線程 1 對共用變數 chenmo 的修改要想被線程 2 及時看到,必須要經過 2 個步驟:
1、把工作記憶體 1 中更新過的共用變數刷新到主記憶體中。
2、將主記憶體中最新的共用變數的值更新到工作記憶體 2 中。
那假如共用變數沒有及時被其他線程看到的話,會發生什麼問題呢?
public class Wanger {
private static boolean chenmo = false;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (!chenmo) {
}
}
});
thread.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
chenmo = true;
}
}
這段代碼的本意是:在主線程中創建子線程,然後啟動它,當主線程休眠 500 毫秒後,把共用變數 chenmo 的值修改為 true 的時候,子線程中的 while 迴圈停下來。但運行這段代碼後,程式似乎進入了死迴圈,過了 N 個 500 毫秒,也沒有要停下來的意思。
為什麼會這樣呢?
因為主線程對共用變數 chenmo 的修改沒有及時通知到子線程(子線程在運行的時候,會將 chenmo 變數的值拷貝一份放在自己的工作記憶體當中),當主線程更改了 chenmo 變數的值之後,但是還沒來得及寫入到主存當中,那麼子線程此時就不知道主線程對 chenmo 變數的更改,因此還會一直迴圈下去。
換句話說,就是:普通的共用變數不能保證可見性,因為普通共用變數被修改之後,什麼時候被寫入主記憶體是不確定的,當其他線程去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。
那怎麼解決這個問題呢?
使用 volatile 關鍵字修飾共用變數 chenmo。
因為 volatile 變數被線程訪問時,會強迫線程從主記憶體中重讀變數的值,而當變數被線程修改時,又會強迫線程將最近的值刷新到主記憶體當中。這樣的話,線程在任何時候總能看到變數的最新值。
我們來使用 volatile 修飾一下共用變數 chenmo。
private static volatile boolean chenmo = false;
再次運行代碼後,程式在一瞬間就結束了,500 毫秒畢竟很短啊。在主線程(main 方法)將 chenmo 修改為 true 後,chenmo 變數的值立即寫入到了主記憶體當中;同時,導致子線程的工作記憶體中緩存變數 chenmo 的副本失效了;當子線程讀取 chenmo 變數時,發現自己的緩存副本無效了,就會去主記憶體讀取最新的值(由 false 變為 true 了),於是 while 迴圈也就停止了。
也就是說,在某種場景下,我們可以使用 volatile 關鍵字來安全地共用變數。這種場景之一就是:狀態真正獨立於程式內地其他內容,比如一個布爾狀態標誌(從 false 到 true,也可以再轉換到 false),用於指示發生了一個重要的一次性事件。
至於 volatile 的原理和實現機制,本篇不再深入展開了(小編自己沒搞懂,尷尬而不失禮貌的笑一笑)。
需要再次強調地是:
volatile 變數可以被看作是一種 “程度較輕的 synchronized”;與 synchronized 相比,volatile 變數運行時地開銷比較少,但是它所能實現的功能也僅是 synchronized 的一部分(只能確保可見性,不能確保原子性)。
原子性我們上一篇已經討論過了,增量操作(i++)看上去像一個單獨操作,但實際上它是一個由“讀取-修改-寫入”組成的序列操作,因此 volatile 並不能為其提供必須的原子特性。
除了 volatile 和 synchronized,Lock 也能夠保證可見性,它能保證同一時刻只有一個線程獲取鎖然後執行同步代碼,並且在釋放鎖之前會將對變數的修改刷新到主存當中。關於 Lock 的更多細節,我們後面再進行討論。
好了,共用變數的可見性就先介紹到這。希望本篇文章能夠對大家有所幫助,謝謝大家的閱讀。
05、最後
謝謝大家的閱讀,原創不易,喜歡就點個贊,這將是我最強的寫作動力。如果你覺得文章對你有所幫助,也蠻有趣的,就關註一下「沉默王二」公眾號。