深入理解Java內存模型詳解

原子性
原子性即一個操作或一系列是不可中斷的。即使是在多個線程的情況下,操作一旦開始,就不會被其他線程干擾。

比如,對於一個靜態變量int x兩條線程同時對其賦值,線程A賦值爲1,而線程B賦值爲2,不管線程如何運行,最終x的值要麼是1,要麼是2,線程A和線程B間的操作是沒有干擾的,這就是原子性操作,不可被中斷的。

Java內存模型對以下操作保證其原子性:read,load,assign,use,store,write。我們可以大致認爲基本數據類型的訪問讀寫是具備原子性的(前面也提到了long和double類型的“半個變量”情況,不過幾乎不會發生)。

從Java內存模型底層來看有上面的原子性操作,但針對用戶來說,也就是我們編寫Java的程序,如果需要更大範圍的原子性保障,就需要同步關鍵字——synchronized來保障了。也就是說synchronized中的操作也具有原子性。

可見性
可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。

Java內存模型是通過變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值,將主內存作爲傳遞媒介。可回顧一下上篇文章的圖。

無論普通變量還是volatile變量都是如此,只不過volatile變量保證新值能夠立馬同步到主內存,使用時也立即從主內存刷新,保證了多線程操作時變量的可見性。而普通變量不能夠保證。

除了volatile,synchronized和final也能夠實現可見性。

synchronized實現的可見性是通過“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中”來保證的。

主要有兩個原則:線程解鎖前,必須把共享變量的最新值刷新到主內存中;線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。

final的可見性是指:被final修飾的字段在構造器中一旦初始化完成,並且構造器沒有把“this”的引用傳遞出去,那在其他線程中就能看見final的值。

有序性
在Java內存模型中有序性可歸納爲這樣一句話:如果在本線程內觀察,所有操作都是有序的,如果在一個線程中觀察另一個線程,所有操作都是無序的。

有序性是指對於單線程的執行代碼,執行是按順序依次進行的。但在多線程環境中,則可能出現亂序現象,因爲在編譯過程會出現“指令重排”,重排後的指令與原指令的順序未必一致。

因此,上面歸納的前半句指的是線程內保證串行語義執行,後半句則指指“令重排現”象和“工作內存與主內存同步延遲”現象。

同樣,Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性。

指令重排
計算機執行指令經過編譯之後形成指令序列。一般情況,指令序列是會輸出確定的結果,且每一次的執行都有確定的結果。

CPU和編譯器爲了提升程序執行的效率,會按照一定的規則允許進行指令優化。但代碼邏輯之間是存在一定的先後順序,併發執行時按照不同的執行邏輯會得到不同的結果。

編譯器優化重排序:編譯器在不改變單線程程序語義的前提下,重新安排語句執行順序。

指令級並行重排序:處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應及其的執行順序。

內存系統的重排序:處理器使用緩存和讀/寫緩衝區,使得加載和存儲操作看上去可能是亂序執行。

舉個例來說明一下多線程中可能出現的重排現象:

class ReOrderDemo {    int a = 0;    boolean flag = false;    public void write() {        a = 1;                   //1        flag = true;             //2    }        public void read() {        if (flag) {                //3            int i =  a * a;        //4            ……        }    } }

在上面的代碼中,單線程執行時,read方法能夠獲得flag的值進行判斷,獲得預期結果。但在多線程的情況下就可能出現不同的結果。

比如,當線程A進行write操作時,由於指令重排,write方法中的代碼執行順序可能會變成下面這樣:

flag = true;             //2a = 1;                   //1

也就是說可能會先對flag賦值,然後再對a賦值。這在單線程中並不影響最終輸出的結果。

但如果與此同時,B線程在調用read方法,那麼就有可能出現flag爲true但a還是0,這時進入第4步操作的結果就爲0,而不是預期的1了。

請記住,指令重排只會保證單線程中串行語義執行的一致性,不會關心多線程間語義的一致性。這也是爲什麼在寫單例模式時需要考慮添加volatile關鍵詞來修飾,就是爲了防止指令重排導致的問題。

JMM提供的解決方案
在瞭解了原子性、可見性以及有序性問題後,看看JMM是提供了什麼機制來保證這些特性的。

原子性問題,除了JVM自身提供的對基本數據類型讀寫操作的原子性外,對於方法級別或者代碼塊級別的原子性操作,可以使用synchronized關鍵字或者重入鎖(ReentrantLock)保證程序執行的原子性。

而工作內存與主內存同步延遲現象導致的可見性問題,可以使用synchronized關鍵字或者volatile關鍵字解決。它們都可以使一個線程修改後的變量立即對其他線程可見。

對於指令重排導致的可見性問題和有序性問題,則可以利用volatile關鍵字解決。volatile的另一個作用就是禁止重排序優化。

除了靠sychronized和volatile關鍵字之外,JMM內部還定義一套happens-before(先行發生)原則來保證多線程環境下兩個操作間的原子性、可見性以及有序性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章