JMM怎麼解決原子性、可見性、有序性的問題
在Java中提供了一系列和併發處理相關的關鍵字,比如volatile、Synchronized、final、juc等,這些就是Java內存 模型封裝了底層的實現後提供給開發人員使用的關鍵字,在開發多線程代碼的時候,我們可以直接使用 synchronized等關鍵詞來控制併發,使得我們不需要關心底層的編譯器優化、緩存一致性的問題了,所以在Java內 存模型中,除了定義了一套規範,還提供了開放的指令在底層進行封裝後,提供給開發人員使用
原子性
java中提供了兩個高級的字節碼指令monitorenter和monitorexit,在Java中對應的Synchronized來保證代碼塊 內的操作是原子的
可見性
Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後可以立即同步到主內存,被其修飾的變量在每次是用之前都從主內存刷新。因此,可以使用volatile來保證多線程操作時變量的可見性。
除了volatile,Java中的synchronized和final兩個關鍵字也可以實現可見性
有序性
在Java中,可以使用synchronized和volatile來保證多線程之間操作的有序性。實現方式有所區別: volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只允許一條線程操作。
volatile如何保證可見行
volatile變量修飾的共享變量,在進行寫操作的時候會多出一個lock前綴的彙編指令,這個指令在前面CPU 高速緩存的時候提到過,會觸發總線鎖或者緩存鎖,通過緩存一致性協議來解決可見性問題
對於聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,把這個變量所在的緩存行的數 據寫回到系統內存,再根據我們前面提到過的MESI的緩存一致性協議,來保證多CPU下的各個高速緩存中的數據的 一致性。
volatile寫:當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。
volatile讀:當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
volatile防止指令重排序
指令重排的目的是爲了大化的提高CPU利用率以及性能,CPU的亂序執行優化在單核時代並不影響正確性,但是 在多核時代的多線程能夠在不同的核心上實現真正的並行,一旦線程之間共享數據,就可能會出現一些不可預料的 問題
指令重排序必須要遵循的原則是,不影響代碼執行的終結果,編譯器和處理器不會改變存在數據依賴關係的兩個 操作的執行順序,(這裏所說的數據依賴性僅僅是針對單個處理器中執行的指令和單個線程中執行的操作.)
這個語義,實際上就是as-if-serial語義,不管怎麼重排序,單線程程序的執行結果不會改變,編譯器、處理器都必 須遵守as-if-serial語義
volatile重排序規則:
volatile內存語義的實現——JMM對volatile的內存屏障插入策略: 在每個volatile寫操作的前面插入一個StoreStore屏障。在每個volatile寫操作的後面插入一個StoreLoad屏障。 在每個volatile讀操作的後面插入一個LoadLoad屏障。在每個volatile讀操作的後面插入一個LoadStore屏障。
volatile總結:
有volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令:
-
將當前處理器緩存行的數據寫回到系統內存
-
這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。
3.1.2 volatile爲什麼不能保證原子性
public class Demo {
volatile int i;
public void incr(){
i++;
}
public static void main(String[] args) {
new Demo().incr();
}
}
對一個原子遞增的操作,會分爲三個步驟:1.讀取volatile變量的值到local;2.增加變量的值;3.把local的值寫回讓 其他線程可見
volatile只能保證對單次讀/寫的原子性
參考文章
https://www.jianshu.com/p/8a58d8335270 JMM和底層實現原理
https://www.cnblogs.com/yanlong300/p/8986041.html CPU緩存一致性協議MESI