深刻講解併發編程模型之併發三大特性篇

圖片描述

推薦閱讀

閱讀本文以前,建議先閱讀 深刻講解併發編程模型之概念篇 瞭解什麼是重排序、什麼是內存屏障、什麼是 happens-before。否則下面的內容閱讀起來有點費勁。編程


可見性

一個線程的操做結果對其它線程可見成爲可見性緩存

  • volatile:保證對變量的寫操做的可見性
  • synchronized:對變量的讀寫(或者)代碼塊的執行加鎖,執行完畢,操做結果寫回內存,保證操做的可見性

volatite如何保證可見性

在Java中主要是使用了volatite修飾的變量,那麼就能夠保證可見效。工做原理以下:多線程

lock前綴指令和MESI協議綜合使用

對於volatile修飾的變量,執行寫操做的話,JVM會發送一條lock前綴指令給CPU,CPU在計算完以後會當即將這個值寫回主內存,同時由於有MESI緩存一致性協議,因此各個CPU都會對總線進行嗅探本身本地緩存中的數據是否被修改了。若是發現某個緩存的值被修改了,那麼CPU就會將本身本地緩存的數據過時掉,而後這個CPU上執行的線程在讀取那個變量的時候,就會從主內存從新加載最新的數據了。併發

使用lock前綴指令和MESI協議綜合使用保證了可見性。app

synchronized如何保證可見性

synchronized主要對變量讀寫,或者代碼塊的執行進行加鎖,在未釋放鎖以前,其它現場沒法操做synchronized修飾的變量或者代碼塊。而且,在釋放鎖以前會講修改的變量值寫到內存中,其它線程進來時讀取到的就是新的值了。post

原子性

原子性表示一步操做執行過程當中不容許其餘操做的出現,直到該操做的完成。

在Java中,對基本數據類型變量的賦值操做是原子性操做。可是對於複合操做是不具備原子性的,好比:優化

int a = 0; // 具備原子性
a++; // 不具備原子性,這個是複合操做,先讀取a的值,再進行+1操做,而後把+1結果寫給a
int b = a; // 這個也不具備原子性,先讀取a,而後把b值設爲a

在Java的JMM模型中,定義了八種原子操做:spa

  • lock(鎖定):做用於內存中的變量,將變量標識爲某個線程的獨佔狀態
  • unlock(解鎖):做用於內存中的變量,將變量從某個線程的獨佔狀態中釋放出來,供其它現場獲取獨佔狀態
  • read(讀取):從內存中讀取變量到線程的工做內存中,供load操做使用
  • load(載入):做用於線程工做內存,將read從內存讀取的變量,保存到工做內存的變量副本
  • use(使用):做用於工做內存中的變量,當虛擬機執行到須要變量的字節碼時,就會須要該動做
  • assign(賦值):做用於工做內存中的變量,當虛擬機執行變量的賦值字節碼時,將執行該操做,將值賦值給工做內存中的變量
  • store(存儲):做用與工做內存中的變量,將工做內存的變量傳遞給內存
  • write(寫入):做用於內存的變量,將store步驟中傳遞過來的變量,寫入到內存中

有序性

程序執行的順序按照代碼的前後順序執行代碼的執行步驟有序線程

  • valotile:經過禁止指令重排序保證有序性
  • synchronized:經過加鎖互斥其它線程的執行保證可見性

在Java中,處理器和編譯器會對指令進行重排序的。可是這個重排序只是對單個線程內程序執行結果沒有影響,在多線程環境下可能就有影響了。code

int a = 10; // 1
int b = 12; // 2
a = a + 1; // 3
b = b * 2; // 4

實際上,在單線程環境中,程序1和2執行的順序對程序結果沒有影響,程序3和4執行順序對程序執行結果沒有影響,它們是能夠在編譯器或者處理的優化下作指令重排的,可是程序3不會在程序1以前執行,由於這會影響程序執行結果。具體關於指令重排序,推薦閱讀 [深刻講解併發編程模型之重排序篇
](http://www.funcodingman.cn/po...

boolean flag = true; 
flag = false; // 0
int a = 0;
//線程1執行 一、2 代碼
a = 1   // 1
flag = true; // 2    

//線程2執行 三、四、五、6 代碼
while(!flag){ // 3
 a = a + 1; // 4
} // 5
System.out.println(a); // 6

此時,若是有兩個現場執行該段代碼,按照咱們編寫的代碼邏輯思路是,先執行一、2,再執行三、四、五、六、7。可是在多線程環境中,若是指令進行了重排序,致使2先在0以前執行,那麼就會致使預期輸出a是2,那麼實際是1。

因此,在Java中,咱們須要經過valotite、synchronized對程序進行保護,防止指令重排序讓程序輸出不是預期的結果。

保證有序性的重要原則

在Java中,編譯器和處理器要想對指令進行重排序,若是程序符合下面的原則,就不會發生重排序,這是JMM強制要求的。

happens-before 四大原則

  • 程序次序規則:<span style="color:red">一個線程內</span>(不適用多線程),按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做
  • 監視器鎖規則:對一個監視器的解鎖操做先行發生於後面對同一個鎖的佔有鎖操做
  • volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做。也就是程序代碼若是是先寫再讀,那麼就不能重排序先讀再寫。
  • 傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C

若是程序不知足這四大原則的話,原則上是能夠任意重排序的。

volatite 如何保證有序性

內存屏障
LoadLoad內存屏障

Load對應JMM中的加載數據的意思。

語法格式:

// load1表示加載指令1,load2表示加載指令2
load1: LoadLoad :load2

LoadLoad屏障:load1;LoadLoad;load2,確保load1數據的裝載先於load2後全部裝載指令,也就是說,load1對應的代碼和load2對應的代碼,是不能指令重排的

StoreStore內存屏障

Store對應JMM中存儲數據在線程本地工做內存的意思

語法格式:

store1;StoreStore;store2

StoreStore屏障:store1;StoreStore;store2,確保store1的數據必定刷回主存,對其餘cpu可見,先於store2以及後續指令

LoadStore內存屏障

語法格式:

load1;LoadStore;store2

LoadStore屏障:load1;LoadStore;store2,確保load1指令的數據裝載,先於store2以及後續指令

StoreLoad內存屏障

語法格式:

store1;LoadStore;load2

StoreLoad屏障:store1;StoreLoad;load2,確保store1指令的數據必定刷回主存,對其餘cpu可見,先於load2以及後續指令的數據裝載

那麼volatile修飾的變量,如何在內存屏障中體現的呢?

看一段代碼:

volatile a = 1;

a = 2; // store操做

int b = a // load操做

對於volatile修改變量的讀寫操做,都會加入內存屏障

  • 每一個volatile讀操做前面,加LoadLoad屏障,禁止上面的普通讀和voaltile讀重排
  • 每一個volatile讀操做後面,加LoadStore屏障,禁止下面的普通寫和volatile讀重排
  • 每一個volatile寫操做前面,加StoreStore屏障,禁止上面的普通寫和它重排
  • 每一個volatile寫操做後面,加StoreLoad屏障,禁止跟下面的volatile讀/寫重排。

因此,上面代碼的僞指令代碼:

volatile a = 1; // 聲明一個a變量,值爲1

StoreStore; // 禁止上面的a = 1和a=2重排

a = 2;

StoreLoad; // 確保a的值刷回主內存,對全部CPU可見,下面的讀操做纔會執行

int b = a;

總結

這裏和你們詳細分析了併發三大特性問題,分別是可見效、原子性和有序性,以及在Java中如何保證這三大特性,具體的原理是什麼。

推薦閱讀
相關文章
相關標籤/搜索