Java多線程以內存模型

目錄

  • 多線程須要解決的問題
    • 線程之間的通訊
    • 線程之間的同步
  • Java內存模型
    • 內存間的交互操做
    • 指令屏障
    • happens-before規則
  • 指令重排序
    • 從源程序到字節指令的重排序
    • as-if-serial語義
    • 程序順序規則
  • 順序一致性模型
    • 順序一致性模型特性
    • 順序一致性模型特性
    • 當程序未正確同步會發生什麼
  • 參考資料

多線程須要解決的問題

在多線程編程中,線程之間如何通訊和同步是一個必須解決的問題:java

線程之間的通訊:

線程之間有兩種通訊的方式:消息傳遞和共享內存程序員

  • 共享內存:線程之間共享程序的公共狀態,經過讀——寫修改公共狀態進行隱式通訊。如上面代碼中的numLock能夠被理解爲公共狀態
  • 消息傳遞:線程之間沒有公共狀態,必須經過發送消息來進行顯示通訊
    在java中,線程是經過共享內存來完成線程之間的通訊

線程之間的同步:

同步指程序中永固空值不一樣線程間的操做發生的相對順序的機制編程

  • 共享內存:同步是顯示進行的,程序員須要指定某個方法或者某段代碼須要在線程之間互斥執行。如上面代碼中的Lock加鎖和解鎖之間的代碼塊,或者被synchronized包圍的代碼塊
  • 消息傳遞:同步是隱式執行的,由於消息的發送必然發生在消息的接收以前,例如使用Objetc#notify(),喚醒的線程接收信號必定在發送喚醒信號的發送以後。

Java內存模型

在java中,全部的實例域,靜態域、數組都被存儲在堆空間當中,堆內存在線程之間共享。c#

全部的局部變量,方法定義參數和異常處理器參數不會被線程共享,在每一個線程棧中獨享,他們不會存在可見性和線程安全問題。數組

從Java線程模型(JMM)的角度來看,線程之間的共享變量存儲在主內存當中,每一個線程擁有一個私有的本地內存(工做內存)本地內存存儲了該線程讀——寫共享的變量的副本。
JMM只是一個抽象的概念,在現實中並不存在,其中全部的存儲區域都在堆內存當中。JMM的模型圖以下圖所示:
緩存

而java線程對於共享變量的操做都是對於本地內存(工做內存)中的副本的操做,並無對共享內存中原始的共享變量進行操做;安全

以線程1和線程2爲例,假設線程1修改了共享變量,那麼他們之間須要通訊就須要兩個步驟:多線程

  1. 線程1本地內存中修改過的共享變量的副本同步到共享內存中去
  2. 線程2從共享內存中讀取被線程1更新過的共享變量
    這樣才能完成線程1的修改對線程2的可見。

內存間的交互操做

爲了完成這一線程之間的通訊,JMM爲內存間的交互操做定義了8個原子操做,以下表:併發

操做 做用域 說明
lock(鎖定) 共享內存中的變量 把一個變量標識爲一條線程獨佔的狀態
unlock(解鎖) 共享內存中的變量 把一個處於鎖定的變量釋放出來,釋放後其餘線程能夠進行訪問
read(讀取) 共享內存中的變量 把一個變量的值從共享內存傳輸到線程的工做內存。供隨後的load操做使用
load(載入) 工做內存 把read操做從共享內存中獲得的變量值放入工做內存的變量副本當中
use(使用) 工做內存 把工做內存中的一個變量值傳遞給執行引擎
assign(賦值) 工做內存 把一個從執行引擎接受到的值賦值給工做內存的變量
store(存儲) 做用於工做內存 把一個工做內存中的變量傳遞給共享內存,供後續的write使用
write(寫入) 共享內存中的變量 把store操做從工做內存中獲得的變量的值放入主內存

JMM規定JVM四線時必須保證上述8個原子操做是不可再分割的,同時必須知足如下的規則:app

  1. 不容許readloadstorewrite操做之一單獨出現,即不容許只從共享內存讀取但工做內存不接受,或者工做捏村發起回寫可是共享內存不接收
  2. 不容許一個線程捨棄assign操做,即當一個線程修改了變量後必須寫回工做內存和共享內存
  3. 不容許一個線程將未修改的變量值寫回共享內存
  4. 變量只能從共享內存中誕生,不容許線程直接使用未初始化的變量
  5. 一個變量同一時刻只能由一個線程對其執行lock操做,可是一個變量能夠被同一個線程重複執行屢次lock,可是須要相同次數的unlock
  6. 若是對一個變量執行lock操做,那麼會清空工做內存中此變量的值,在執行引擎使用這個變量以前須要從新執行load和assign
  7. 不容許unlock一個沒有被鎖定的變量,也不容許unlock一個其餘線程lock的變量
  8. 對一個變量unlock以前必須把此變量同步回主存當中。

longdouble的特殊操做
在一些32位的處理器上,若是要求對64位的longdouble的寫具備原子性,會有較大的開銷,爲了照固這種狀況,
java語言規範鼓勵但不要求虛擬機對64位的longdouble型變量的寫操做具備原子性,當JVM在這種處理器上運行時,
可能會把64位的long和double拆分紅兩次32位的寫

指令屏障

爲了保證內存的可見性,JMM的編譯器會禁止特定類型的編譯器從新排序;對於處理器的從新排序,
JMM會要求編譯器在生成指令序列時插入特定類型的的內存屏障指令,經過內存屏障指令巾紙特定類型的處理器從新排序

JMM規定了四種內存屏障,具體以下:

屏障類型 指令示例 說明
LoadLoad Barriers Load1;LoadLoad;Load2 確保Load1的數據先於Load2以及全部後續裝在指令的裝載
StoreStore Barries Store1;StoreStore;Store2 確保Store1數據對於其餘處理器可見(刷新到內存)先於Store2及後續存儲指令的存儲
LoadStore Barriers Load1;LoadStore;Store2 確保Load1的裝載先於Store2及後續全部的存儲指令
StoreLoad Barrier Store1;StoreLoad;Load2 確保Store1的存儲指令先於Load1以及後續所全部的加載指令

StoreLoad是一個「萬能」的內存屏障,他同時具備其餘三個內存屏障的效果,現代的處理器大都支持該屏障(其餘的內存屏障不必定支持),
可是執行這個內存屏障的開銷很昂貴,由於須要將處理器緩衝區全部的數據刷回內存中。

happens-before規則

在JSR-133種內存模型種引入了happens-before規則來闡述操做之間的內存可見性。在JVM種若是一個操做的結果過須要對另外一個操做可見,
那麼兩個操做之間必然要存在happens-bsfore關係:

  • 程序順序規則:一個線程中的個每一個操做,happens-before於該線程的後續全部操做
  • 監視器鎖規則:對於一個鎖的解鎖,happens-before於隨後對於這個鎖的加鎖
  • volatitle變量規則:對於一個volatile的寫,happens-before於認意後續對這個volatile域的讀
  • 傳遞性:若是A happens-before B B happends-beforeC,那麼A happends-before C

指令重排序

從源程序到字節指令的重排序

衆所周知,JVM執行的是字節碼,Java源代碼須要先編譯成字節碼程序才能在Java虛擬機中運行,可是考慮下面的程序;

int a = 1;
int b = 1;

在這段代碼中,ab沒有任何的相互依賴關係,所以徹底能夠先對b初始化賦值,再對a變量初始化賦值;

事實上,爲了提升性能,編譯器和處理器一般會對指令作從新排序。重排序分爲3種:

  1. 編譯器優化的重排序。編譯器在不改變單線程的程序語義的前提下,能夠安排字語句的執行順序。編譯器的對象是語句,不是字節碼,
    可是反應的結果就是編譯後的字節碼和寫的語句順序不一致。
  2. 執行級並行的重排序。現代處理器採用了並行技術,來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
  3. 內存系統的重排序,因爲處理器使用了緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

數據依賴性:若是兩個操做訪問同一個變量,且兩個操做有一個是寫操做,則這兩個操做存在數據依賴性,改變這兩個操做的執行順序,就會改變執行結果。

儘管指令重排序會提升代碼的執行效率,可是卻爲多線程編程帶來了問題,多線程操做共享變量須要必定程度上遵循代碼的編寫順序,
也須要將修改的共享數據存儲到共享內存中,不按照代碼順序執行可能會致使多線程程序出現內存可見性的問題,那又如何實現呢?

as-if-serial語義

as-if-serial語義:不論程序怎樣進行重排序,(單線程)程序的執行結果不能被改變。編譯器、runtime和處理器都必須支持as-if-serial語義。

程序順序規則

假設存在如下happens-before程序規則:

1) A happens-before B
    2) B happens-before C
    3) A happens-before C

儘管這裏存在A happens-before B這一關係,可是JMM並不要求A必定要在B以前執行,僅僅要求A的執行結果對B可見。
即JMM僅要求前一個操做的結果對於後一個操做可見,而且前一個操做按照順序排在後一個操做以前。
可是若前一個操做放在後一個操做以後執行並不影響執行結果,則JMM認爲這並不違法,JMM容許這種重排序。

順序一致性模型

在一個線程中寫一個變量,在另外一個線程中同時讀取這個變量,讀和寫沒有經過排序來同步來排序,就會引起數據競爭。

數據競爭的核心緣由是程序未正確同步。若是一個多線程程序是正確同步的,這個程序將是一個沒有數據競爭的程序。

順序一致性模型只是一個參考模型。

順序一致性模型特性

  • 一個線程中全部的操做必須按照程序的順序來執行。
  • 無論線程是否同步,全部的線程都只能看到一個單一的執行順序。

在順序一致性模型中每一個曹祖都必須原子執行且馬上對全部線程可見。

當程序未正確同步會發生什麼

當線程未正確同步時,JMM只提供最小的安全性,當讀取到一個值時,這個值要麼是以前寫入的值,要麼是默認值。
JMM保證線程的操做不會無中生有。爲了保證這一特色,JMM在分配對象時,首先會對內存空間清0,而後纔在上面分配對象。

未同步的程序在JMM種執行時,總體上是無序的,執行結果也沒法預知。位同步程序子兩個模型中執行特色有以下幾個差別:

  • 順序一致性模型保證單線程內的操做會按照程序的順序執行,而JMM不保證單線程內的操做會按照程序的順序執行
  • 順序一致性模型保證全部線程只能看到一致的操做執行順序,而JMM不保證全部線程能看到一致的操做執行順序
  • JMM不保證對64位的longdouble型變量具備寫操做的原子性,而順序一致性模型保證對全部的內存的讀/寫操做都具備原子性

參考資料

java併發編程的藝術-方騰飛,魏鵬,程曉明著

相關文章
相關標籤/搜索