細說Java 多執行緒之記憶體可見性
前言:
討論學習Java中的記憶體可見性、Java記憶體模型、指令重排序、as-if-serial語義等多執行緒中偏向底層的一些知識,以及synchronized和volatile實現記憶體可見性的原理和方法。
1、可見性介紹
可見性:一個執行緒對共用變數值的修改,能夠及時地被其他執行緒看到。
共享變數:如果一個變數在多個執行緒的工作記憶體中都存在副本,那麼這個變數就是這個幾個執行緒的共享變數。‘
’Java記憶體模型中
- 所有的變數都儲存在主記憶體中
- 每個執行緒都有自己獨立的工作記憶體,並且裡面儲存該執行緒中使用的變數的副本(主記憶體中該變數的一份拷貝)
- 執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能直接從主記憶體中讀寫。
- 不同執行緒之間無法直接訪問其他執行緒工作記憶體中的變數,執行緒間變數值的傳遞需要通過主記憶體來完成。
2、共享變數可見性實現原理
執行緒1對共享變數的修改要想被執行緒2及時看到,必須經過以下兩個步驟:
- 把工作記憶體1中更新過的共享變數重新整理到主記憶體中;
- 將主記憶體中最新的共享變數的值更新到工作記憶體2中。
問題:多執行緒中怎樣保證共享變數的可見性?
方案:Java語言層面支援的可見性實現方式為:
- synchronized
- volatile
3、synchronized實現可見性
synchronized 的兩個功能:
- 原子性(同步)
- 可見性
synchronized 實現可見性的原因:
JMM關於synchronized的兩條規定
- 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中
- 執行緒加鎖時,清空工作記憶體中共享變數,使用共享變數時需要從主記憶體中重新獲取最新的值(解鎖和加鎖同一把鎖)
執行緒解鎖前對共享變數的修改,在下次加鎖時對其他執行緒可見。
synchronized 實現可見性的過程:
指令重排序:程式碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或者處理器為了提高程式效能而做的優化。
指令重排序一般有三種:
- 編譯器優化的重排序
- 指令級並行的重排序
- 記憶體系統的重排序
as-if-serial:無論如何重排序,程式執行的結果應該與程式碼順序執行的結果一致。(Java 編譯器、執行時和處理器都會保證Java在單執行緒下遵循as-if-serial )
問題:重排序不會給單執行緒帶來記憶體可見性問題,但是在多執行緒中程式交錯執行時,重排序可能會造成記憶體可見性的問題?
多執行緒下造成記憶體可見性問題DEMO
多執行緒中造成記憶體不可見性的原因:
- 執行緒的交叉執行
- 重排序結合線程交叉進行
- 共享變數更新後的值沒有及時在工作記憶體與主記憶體之間及時更新
解決方案:
方案分析:
4、volatile實現可見性
volatile關鍵字:
- 能夠保證volatile變數的可見性
- 不能保證volatile變數複合操作的原子性
volatile 實現可見性的原理
通過加入記憶體屏障和禁止重排序優化來實現的
- 對volatile變數執行寫操作時,會在寫操作後加入一條store屏障指令,該指令強制共享變數重新整理到主記憶體中。
- 對volatile變數執行讀操作時,會在讀操作前加入一條load屏障指令,該指令強制工作記憶體中共享變數失效,並且從主記憶體中讀取最新值。
volatile 不能保證原子性的demo
保證原子性的方案:
- synchronized
- ReentrantLock
使用volatile的場景
5、總結
synchronized 與 volatile 比較
- volatile 不需要加鎖,比synchronized 更輕量級,
- 從記憶體可見性看,volatile讀相當於加鎖,volatile寫相當於解鎖
- synchronized 保證可見性和原子性,而volatile 只保證可見性