在《Java內存模型(JMM)詳解》一文中咱們已經講到了Java內存模型的基本結構以及相關操做和規則。而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內存模型是經過變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值,將主內存做爲傳遞媒介。可回顧一下上篇文章的圖。app
不管普通變量仍是volatile變量都是如此,只不過volatile變量保證新值可以立馬同步到主內存,使用時也當即從主內存刷新,保證了多線程操做時變量的可見性。而普通變量不可以保證。jvm
除了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; //2 a = 1; //1
也就是說可能會先對flag賦值,而後再對a賦值。這在單線程中並不影響最終輸出的結果。
但若是與此同時,B線程在調用read方法,那麼就有可能出現flag爲true但a仍是0,這時進入第4步操做的結果就爲0,而不是預期的1了。
請記住,指令重排只會保證單線程中串行語義執行的一致性,不會關心多線程間語義的一致性。這也是爲何在寫單例模式時須要考慮添加volatile關鍵詞來修飾,就是爲了防止指令重排致使的問題。
在瞭解了原子性、可見性以及有序性問題後,看看JMM是提供了什麼機制來保證這些特性的。
原子性問題,除了JVM自身提供的對基本數據類型讀寫操做的原子性外,對於方法級別或者代碼塊級別的原子性操做,可使用synchronized關鍵字或者重入鎖(ReentrantLock)保證程序執行的原子性。
而工做內存與主內存同步延遲現象致使的可見性問題,可使用synchronized關鍵字或者volatile關鍵字解決。它們均可以使一個線程修改後的變量當即對其餘線程可見。
對於指令重排致使的可見性問題和有序性問題,則能夠利用volatile關鍵字解決。volatile的另外一個做用就是禁止重排序優化。
除了靠sychronized和volatile關鍵字以外,JMM內部還定義一套happens-before(先行發生)原則來保證多線程環境下兩個操做間的原子性、可見性以及有序性。
若是僅靠sychronized和volatile關鍵字來保證原子性、可見性以及有序性,那麼編寫併發程序會十分麻煩。爲此在Java內存模型中,還提供了happens-before原則來輔助保證程序執行的原子性、可見性以及有序性的問題。該原則是判斷數據是否存在競爭、線程是否安全的依據。
happens-before規則:
還拿上面的具體代碼來進行說明:
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 …… } } }
線程A調用write()方法,線程B調用read()方法,線程A先(時間上的前後)於線程B啓動,那麼線程B讀取到a的值是多少呢?
如今依據8條原則來進行對照。
兩個方法分別由線程A和線程B調用,不在同一個線程中,所以程序次序原則不適用。
沒有write()方法和read()方法都沒有使用同步手段,監視器鎖規則不適用。
沒有使用volatile關鍵字,volatile變量原則不適用。
與線程啓動、終止、中斷、對象終結規則、傳遞性都沒有關係,不適用。
所以,線程A和線程B的啓動時間雖然有前後,但線程B執行結果倒是不肯定,也是說上述代碼沒有適合8條原則中的任意一條,因此線程B讀取的值天然也是不肯定的,換句話說就是線程不安全的。
修復這個問題的方式很簡單,要麼給write()方法和read()方法添加同步手段,如synchronized。或者給變量flag添加volatile關鍵字,確保線程A修改的值對線程B老是可見。
在這篇文章中介紹了Java內存模型中一些原則,及其衍生出來保證這些原則的方式和方法。也是爲咱們下面學習volatile這個面試官最愛問的關鍵字的作好鋪墊。歡迎關注微信公衆號「程序新視界」繼續學習。
原文連接:《Java內存模型相關原則詳解》
《面試官》系列文章:
<center>程序新視界:精彩和成長都不容錯過</center>