Java 中的多執行緒之記憶體可見性
阿新 • • 發佈:2019-01-22
1. Java記憶體模型(JMM)
Java 記憶體模型(Java Memory Model)描述了Java程式中各種變數(執行緒共享變數)的訪問規則,以及在 JVM 中將變數儲存到記憶體和從記憶體中讀取出變數的底層細節。
- 所有的變數都儲存在主記憶體中
- 每個執行緒都有自己獨立的工作記憶體,裡面儲存該執行緒使用到的變數副本(主記憶體中該變數的一份拷貝)
- 通常稱被多個執行緒訪問的變數為共享變數。
2. Java 記憶體模型的兩條規定
執行緒1對共享變數的修改要想被執行緒2及時看到,必須經過下列兩個步驟:
- 把工作記憶體1中更新過的共享變數重新整理到主記憶體中
- 將主記憶體中最新的共享變數的值更新到工作記憶體2中
3. 實現可見性的要求
- 執行緒修改後的共享變數值能夠及時從工作記憶體重新整理到主記憶體中
- 其他執行緒能夠及時把共享變數的最新值從主記憶體更新到自己的工作記憶體中
4. Java 語言層面支援的可見性實現方式
- synchronized 實現
- volatile 關鍵字
4.1 synchronize 實現可見性的過程
- 獲取互斥鎖
- 清空工作記憶體
- 從主記憶體拷貝變數的最新副本到工作記憶體
- 執行程式碼
- 將更改後的共享變數的值重新整理到主記憶體
- 釋放互斥鎖
4.2 volatile 實現可見性
深入來說:通過加入記憶體屏障和禁止重排序優化來實現的,強制快取區快取失效。
對 volatile 變數執行寫操作時,會在寫操作後加入一條 store 屏障指令
對 volatile 變數執行讀操作的時候,會在讀操作前加入一條 load 屏障指令
通俗來說:volatile 變數在每次被執行緒訪問的時候,都強迫從主記憶體中重讀該變數的值,而當該變數發生變化時,又會強迫執行緒將最新的值重新整理到主記憶體。這樣任何時刻,不同的執行緒總能看到該變數的最新值。
執行緒寫 volatile 變數的過程:
1. 改變執行緒工作記憶體中 volatile 變數副本的值
2. 將改變後的副本的值從工作記憶體重新整理到主記憶體
執行緒讀 volatile 變數的過程:
1. 從主記憶體中讀取 volatile 變數的最新值到執行緒的工作記憶體中
2. 從工作記憶體中讀取 volatile 變數的副本
volatile 不能保證變數符合操作的原子性。比如 number ++
1. 從記憶體中取出 number 的值。
2. 計算 number 的值。
3. 將 number 的值寫到記憶體中。
假如在第二步計算值的時候,另外一個執行緒也修改了 number 的值,那麼這個時候就會出現髒資料。
那麼如何保證它的原子性,有以下三種方式:
- 使用 synchronized 關鍵字
- 使用 ReentrantLock ( java.util.concurrent.locks 包下面)
- 使用 AtomicInterger ( java.util.concurrent.atomic 包下面)
演示:
Lock lock = new ReentrantLock();
lock.lock();
try{
this.number ++;
}finally{
lock.unlock();
}
4.3 volatile 和 synchronized 比較
- volatile 不需要加鎖,比 synchronized 輕,不會阻塞執行緒,所以效率比較高,但穩定性差,比如上面說的不能保證原子性。
總而言之:synchronized 穩定性高,效率低;volatile 穩定性低,效率高。 - 對於可見性:
當一個共享變數被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。
而普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。
另外,通過 synchronized 和 Lock 也能夠保證可見性,synchronized 和 Lock 能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。 - volatile 解決的是變數在多個執行緒之間的可見性;而 synchronized 解決的是多個執行緒之間訪問資源的同步性。
5. 導致共享變數線上程間不可見的原因
- 執行緒的交叉執行
- 重排序結合線程的交叉執行
- 共享變數更新後的值沒有在工作記憶體與主記憶體間及時更新