學習內容
1、共享變量線程間的可見性
2、synchronized實現可見性
3、volatile實現可見性
- 指令重排序
- as-if-serial語義
- volatile使用注意事項
3、synchronized和volatile比較
可見性介紹
可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到。
共享變量:如果一個變量在多個線程的工作內存中都存在副本,那麼這個變量就是這幾個線程的共享變量。
Java內存模型(JMM)
Java內存模型(JAVA Memory Model)描述了Java程序中各種變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取出變量這樣的底層細節。
所有的變量都存儲在主內存中
每個線程都有自己獨立的工作內存,裏面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)
兩條規定
1、線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接從主內存中讀寫
2、不同線程之間無法直接訪問其他線程工作內存中的變量,線程間變量值的傳遞需要通過主內存來完成。
共享變量可見性實現的原理
線程1對共享變量的修改要想被線程2及時看到,必須要經過如下2個步驟:
1、把工作內存1中更新過的共享變量刷新到主內存中
2、將主內存中最新的共享變量的值更新到工作內存2中
synchronized實現可見性原理
可見性
要實現共享變量的可見性,必須保證兩點:
1、線程修改後的共享變量值能夠及時從工作內存刷新到主內存中
2、其他線程能夠及時把共享變量的最新值從主內存更新到自己的工作內存中
Java語言層面支持的可見性實現方式
1、synchronized 2、volatile
synchronized能夠實現
1、原子性(同步) 2、可見性
JMM關於synchronized的兩條規定
1、線程解鎖前,必須把共享變量的最新值刷新到主內存中
2、線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值(注意:加鎖與解鎖需要是同一把鎖)
線程解鎖前對共享變量的修改在下次加鎖時對其他線程可見
線程執行互斥代碼的過程:
1、獲得互斥鎖
2、清空工作內存
3、從主內存拷貝變量的最新副本到工作內存
4、執行代碼
5、將更改後的共享變量的值刷新到主內存
6、釋放互斥鎖
重排序
重排序:代碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器爲了提高程序性能而做的優化
1、編譯器優化的重排序(編譯器優化)
2、指令級並行重排序(處理器優化)
3、內存系統的重排序(處理器優化)
as-if-serial
as-if-serial:無論如何重排序,程序執行的結果應該與代碼順序執行的結果一致(Java編譯器、運行時和處理器都會保證Java在單線程下遵循as-if-serial語義)
可見性分析
volatile實現可見性
volatile關鍵字:
1、能夠保證volatile變量的可見性
2、不能保證volatile變量複合操作的原子性
volatile如何實現內存可見性
深入來說:通過加入內存屏障和禁止重排序優化來實現的。
1、對volatile變量執行讀寫操作時,會在寫操作後加入一條store屏障指令
2、對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
通俗來講:volatile變量在每次被線程訪問時,都強迫從主內從中重讀該變量的值,而當該變量發生變化時,又會強迫現場將最新的值刷新到主內存。這樣任何時刻,不同的線程總能看到該變量的最新值
線程寫volatile變量的過程
1、改變線程工作內存中volatile變量副本的值
2、將改變後的副本的值從工作內存刷新到主內存
線程讀volatile變量的過程
1、從主內存中讀取volatile變量的最新值到線程的工作內存中
2、從工作內存中讀取volatile變量的副本
volatile不能保證volatile變量複合操作的原子性:
private int number = 0;
number++; 不是原子性操作
1、讀取number的值
2、將number的值加1
3、寫入最新的number的值
解決辦法:
synchronized(this) {
number++;
}
加入synchronized,變爲原子操作
private volatile int number = 0; 變爲volatile變量,無法保證原子性
保證number自增操作的原子性:
1、使用synchronized關鍵字
2、使用ReentrantLock (java.util.concurrent.lock包下)
3、使用AtomicInteger (java.util.concurrent.atomic包下)
縮小鎖粒度
將lock解鎖語句放在try finally語句中,是JDK推薦的標準寫法
volatile使用注意事項
1、volatile適用場合
要在多線程中安全的使用volatile變量,必須同時滿足:
1、對變量的寫入操作不依賴其當前值
不滿足:number++、count = count * 5等
滿足:boolean變量、記錄溫度變化的變量等
2、該變量沒有包含在具有其他變量的不變式中
不滿足:不變式 low < up
不變式解釋:就是在一個表達式中不能同時含有兩個或兩個以上的volatile變量
synchronized和volatile比較
1、volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程;
2、從內存可見性角度講,volatile讀相當於加鎖,volatile寫相當於解鎖。
3、synchronized既能保證可見性,又能保證原子性,而volatile只能保證可見性,無法保證原子性。
總結
1、什麼是內存可見性
2、Java內存模型(JMM)
3、實現可見性的方式:synchronized和valatile
final也可以保證內存可見性
synchronized和volatile實現內存可見性的原理
4、synchronized實現可見性
指令重排序
as-if-serial語義
5、volatile實現可見性
volatile能夠保證可見性
volatile不能保證原子性
volatile使用注意事項
問:即使沒有保證可見性的措施,很多時候共享變量依然能夠在主內存和工作內存見得到及時的更新?
答:一般只有在短時間內高併發的情況下才會出現變量得不到及時更新的情況,因爲CPU執行時會很快地刷新緩存,所以一般情況下很難看到這種問題。
對64位(long、double)變量的讀寫可能不是原子操作:
Java內存模型允許JVM將沒有被volatile修飾的64位數據類型的讀寫操作劃分爲兩次32位的讀寫操作來進行
導致問題:有可能會出現讀取到“半個變量”的情況
解決方法:加volatile關鍵字