JMM怎麼解決原子性、可見性、有序性的問題?java
- 在Java中提供了一系列和併發處理相關的關鍵字,
- 好比volatile、Synchronized、final、juc(java.util.concurrent)等,
- 這些就是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 防止重排序併發
- 指令重排的目的是爲了最大化的提升CPU利用率以及性能,
- CPU的亂序執行優化在單核時代並不影響正確性,
- 可是在多核時代的多線程可以在不一樣的核心上實現真正的並行,
- 一旦線程之間共享數據,就可能會出現一些不可預料的問題
- 指令重排序必需要遵循的原則是,
- 不影響代碼執行的最終結果,
- 編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序,
- 這裏所說的數據依賴性僅僅是針對單個處理器中執行的指令和單個線程中執行的操做.
- 這個語義,實際上就是as-if-serial語義,
- 無論怎麼重排序,單線程程序的執行結果不會改變,
- 編譯器、處理器都必須遵照as-if-serial語義
多核心多線程下的指令重排影響性能
private static int x = 0, y = 0;
private static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(() -> {
b = 1;
y = a;
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("x=" + x + "->y=" + y);
}
若是不考慮編譯器重排序和緩存可見性問題,優化
上面這段代碼可能會出現的結果是:線程
- x=0,y=1;
- x=1,y=0;
- x=1,y=1
- 由於多是前後執行t1/t2,也多是反過來,還多是t1/t2交替執行,
- 可是這段代碼的執行結果也有多是x=0,y=0。
- 這就是在亂序執行的狀況下會致使的一種結果,
- 由於線程t1內部的兩行代碼之間不存在數據依賴,
- 所以能夠把x=b亂序到a=1以前;
- 同時線程t2中的y=a也能夠早於t1中的a=1執行,那麼他們的執行順序多是
- l t1: x=b
l t2:b=1
l t2:y=a
l t1:a=1
- 因此從上面的例子來看,重排序會致使可見性問題。
- 可是重排序帶來的問題的嚴重性遠遠大於可見性,
- 由於並非全部指令都是簡單的讀或寫,好比DCL的部分初始化問題。
- 因此單純的解決可見性問題還不夠,還須要解決處理器重排序問題
內存屏障3d
- 內存屏障須要解決咱們前面提到的兩個問題,
- 一個是編譯器的優化亂序和CPU的執行亂序,
- 咱們能夠分別使用優化屏障和內存屏障這兩個機制來解決
從CPU層面來了解一下什麼是內存屏障
- CPU的亂序執行,
- 本質仍是,因爲在多CPU的機器上,每一個CPU都存在cache,
- 當一個特定數據第一次被特定一個CPU獲取時,
- 因爲在該CPU緩存中不存在,就會從內存中去獲取,
- 被加載到CPU高速緩存中後就能從緩存中快速訪問。
- 當某個CPU進行寫操做時,
- 它必須確保其餘的CPU已經將這個數據從他們的緩存中移除,
- 這樣才能讓其餘CPU安全的修改數據。
- 顯然,存在多個cache時,咱們必須經過一個cache一致性協議來避免數據不一致的問題,
- 而這個通信的過程就可能致使亂序訪問的問題,也就是運行時的內存亂序訪問。
- 如今的CPU架構都提供了內存屏障功能,
- 在x86的cpu中,實現了相應的內存屏障
- 寫屏障(store barrier)、
- 讀屏障(load barrier)
- 全屏障(Full Barrier),
- 主要的做用是
- store barrier稱爲寫屏障
- 至關於storestore barrier,
- 強制全部在storestore內存屏障以前的全部操做,
- 全部在storestore barrier指令以後的store指令,
- 都必須在storestore barrier屏障以前的指令執行完後再被執行。
- 也就是進制了寫屏障先後的指令進行重排序,
- 是的全部store barrier以前發生的內存更新都是可見的
![](http://static.javashuo.com/static/loading.gif)
- load barrier稱爲讀屏障,
- 至關於loadload barrier,
- 強制全部在load barrier讀屏障以後的load指令,
- 都在loadbarrier屏障以後執行。
- 也就是進制對load barrier讀屏障先後的load指令進行重排序,
- 配合store barrier,使得全部store barrier以前發生的內存更新,
- 對load barrier以後的load操做是可見的
![](http://static.javashuo.com/static/loading.gif)
- full barrier成爲全屏障,
- 至關於storeload,是一個全能型的屏障,
- 由於它同時具有前面兩種屏障的效果。
- 強制了全部在storeload barrier以前的store/load指令,都在該屏障以前被執行,
- 全部在該屏障以後的的store/load指令,都在該屏障以後被執行。
- 禁止對storeload屏障先後的指令進行重排序。
![](http://static.javashuo.com/static/loading.gif)
- 總結:
- 內存屏障只是解決順序一致性問題,
- 不解決緩存一致性問題,
- 緩存一致性是由cpu的緩存鎖以及MESI協議來完成的。
- 而緩存一致性協議只關心緩存一致性,不關心順序一致性。
- 因此這是兩個問題
編譯器層面如何解決指令重排序問題
- 在編譯器層面,經過volatile關鍵字,取消編譯器層面的緩存和重排序。
- 保證編譯程序時在優化屏障以前的指令不會在優化屏障以後執行。
- 這就保證了編譯時期的優化不會影響到實際代碼邏輯順序。
- 若是硬件架構自己已經保證了內存可見性,
- 那麼volatile就是一個空標記,不會插入相關語義的內存屏障
- 若是硬件架構自己不進行處理器重排序,有更強的重排序語義,
- 那麼volatile就是一個空標記,不會插入相關語義的內存屏障。
在JMM中把內存屏障指令分爲4類,
- 經過在不一樣的語義下使用不一樣的內存屏障來進制特定類型的處理器重排序,從而來保證內存的可見性
- LoadLoad Barriers, load1 ; LoadLoad; load2 ,
- 確保load1數據的裝載優先於load2及全部後續裝載指令的裝載
- StoreStore Barriers,store1; storestore;store2 ,
- 確保store1數據對其餘處理器可見優先於store2及全部後續存儲指令的存儲
- LoadStore Barries, load1;loadstore;store2,
- 確保load1數據裝載優先於store2以及後續的存儲指令刷新到內存
- StoreLoad Barries, store1; storeload;load2,
- 確保store1數據對其餘處理器變得可見, 優先於load2及全部後續裝載指令的裝載;
- 這條內存屏障指令是一個全能型的屏障,
- 在前面講cpu層面的內存屏障的時候有提到。
- 它同時具備其餘3條屏障的效果
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的值寫回讓其餘線程可見