java內存模型

什麼Java內存模型?

在多核處理器系統中,處理器一般有一級或者多級的內部緩存(CPU參數中常常看到的L1,L2,L3就是),他們既提升了訪問數據的性能(由於數據更接近處理器而不用受內存速度的影響),同時也減小了在共享內存總線時的衝突(由於不少狀況下內部緩存就以及緩存了內存的操做)。 處理器緩存能夠很是明顯的改善性能,但同時它也帶來了一個新的挑戰。 好比說,當有多個處理器(對於多核處理器就是一個核心)同時訪問同一個內存地址的時候,在什麼條件下他們看見的是同一個值呢? 由於不一樣的處理器可能都有本身的緩存, 如何保證多個處理器都必須從內存中去讀取該內存地址的數據呢?html

在處理器層次,一個內存模型爲以下問題定義了必要和高效的條件:java

*當前處理器如何「看見」其餘處理器對內存地址的「寫」操做
*其餘處理器如何「看見」當前處理器對內存地址的「寫」操做

一些處理器提出了一種強內存模型,它要求全部處理器在任什麼時候刻對任何內存地址都看到相同的值, 另一些處理器提出了一種弱內存模型,其實也就是一種特殊的指令: 內存屏障(memory barriers),爲了當前處理器看見其餘處理器對內存的寫或者其餘處理器看見當前處理器的操做它要求刷新或者驗證處理器緩存的數據。 在加鎖(lock)或者解鎖(unlock)的時候內存屏障常常發生。 可是對於高級語言來講它是不可見的。編程

因爲內存屏障的缺失,強內存模型在某些狀況下更容易編程。可是,即便在強內存模型下, 內存屏障也常常是必須的。 他們的位置常常是違反直覺的。 最近的處理器設計中,因爲內存屏障對於貫穿多個處理器和大內存之間的內存一致性作出的保證,更傾向於弱內存模型數組

因爲編譯器重排序,線程對內存地址的寫操做是否對其餘線程也可見這個問題變得更加複雜。 編譯器可能會認爲移動某個寫操做在後面是更高效的(這裏涉及到編譯器的優化策略, 典型的是循環體中不會改變的變量做爲循環條件,而後在其餘線程修改,可是根據語義分析,編譯器會以爲循環條件不會改變而把變量移動位置 ),固然,這些操做都不會改變程序的語義。 因此若是編譯器延遲或者提早了某個操做,這會很明顯的影響其餘線程看到的數據。 這些折射出了緩存緩存的影響。緩存

更廣泛的是,程序中的寫操做會被向前移動。 這種狀況下,其餘線程可能會看到一個尚未真正發生的「寫」操做。 這些靈活性都是特地設計的: 給予編譯器、運行環境或者硬件在最優的順序執行代碼的靈活性。在內存模型的範疇內,咱們能夠達到更高的性能。
考慮以下代碼:安全

  1. ClassReordering{
  2. int x =0, y =0;
  3. publicvoid write(){
  4. x =1;
  5. y =2;
  6. }
  7. publicvoid reader(){
  8. int r1 = y ;
  9. int r2 = x;
  10. }
  11. }

多線程狀況下,上述代碼中的reader將會看到y=2 ,由於y在x以後被賦值。編寫代碼的可能認爲x的值必定是1,可是,賦值操做極可能被重排序。 好比說 ,對y的賦值可能先於對x的賦值。 此時,若是對y的賦值完成事後,線程就調用了reader方法,那麼r1將會是2,r2倒是0 。 因爲重排序帶來的不肯定性,r1和r2的值徹底沒法肯定。多線程


Java內存模型描述了多線程環境下哪一種代碼是合法的,線程和內存是如何相互影響。它描述了低層次的存儲細節和程序變量、從真實系統中內存或者寄存器讀或者寫數據的關係。經過多種硬件以及多種編譯器優化來實現該模型。
Java包含多種語言指令,好比 volatile, final ,synchronized, 他們向編譯器描述了併發程序的要求。 Java內存模型定義了volatilesynchronized的行爲,最重要的,它肯定在全部多種處理器平臺上具備相同的行爲。 也就是說,java內存模型抽象了處理器相關的併發程序處理細節,提供了一個與具體平臺無關的、語義確保獲得保證的併發抽象層。
併發


其餘語言是否有內存模型

C以及C++語言的多線程程序都是與具體編譯器、操做系統、處理器強相關的。 不一樣平臺下的代碼不兼容。oracle

JSR133S講的是什麼?

1997年開始,在java語言規範的17章就有了java內存模型的定義。 它定義了一些看起來是使人困惑的行爲(好比final字段可能看起來改變了值)。 它也阻礙了編譯器進行通用優化。app

Java內存模型是一個充滿雄心壯志的願景。它是第一次有語言規範提出能夠保證在併發在多種處理器之間有相同語義的內存模型。不幸的是,實現起來比想象中的還要困難。 JSR133提出了一個修復了以前的問題的一個新的內存模型。同時,改變了final和volatile 的語義。

JSR133的目標包括:

  • 保留現存的安全保證,好比類型安全。 增強了其餘的,好比說,變量的值不會無中生有的被建立:線程必須有存儲變量值的「正當」理由(其實就是爲了能看到一樣的值)
  • 同步程序代碼必須足夠簡單、易懂
  • 定義不正確、不徹底的同步程序語義, 以最小化安全風險
  • 程序編寫者可以清楚的知道多線程程序如何和內存相互影響的(即Java內存模型提供的語義在任何平臺都使用而且語義相同)
  • 能夠編寫使用多種流行處理器的高效JVM實現
  • 一個新的初始化安全性的保證。若是一個對象以及被正確的構造(意味着它的引用沒有超出它的構造函數),那麼全部看得見該引用的線程都會看見相同的構造函數中賦值的final成員變量值,即便沒有使用同步(synchronization)。
  • 多現存代碼儘量小的影響

重排序是什麼?

有許多程序變量(類實例變量、類靜態變量、數組)並無按照代碼中指定的順序運行的例子,編譯器爲了優化能夠自由選擇指令順序(語義一致的狀況),處理器也可能亂序執行指令。數據可能按照徹底不一樣代碼中的順序在處理器、處理器緩存、內存中移動。
好比說, 若是某個線程中先對變量a賦值,而後對變量b賦值, b的賦值不依賴於a的狀況下,編譯器能夠自由改變他們的順序。處理器緩存也可能在a以前就把b的值刷新到內存中去。 有許多可能的重排序源,好比編譯器,JIT,CPU緩存。在現代處理器中,亂序執行、分支預測等手段都會致使這個問題。
編譯器、運行時環境、硬件協同構造了一種「順序執行」的假象,這意味着在單線程程序中程序是徹底不會受到重排序的影響, 由於代碼真的像是順序執行下來的。 可是沒有正確使用同步的多線程狀況下,線程之間是否能看見相同的值或者看到改變徹底是隨機性的。 由於重排序使得每一個線程可能都不是按照代碼中的順序執行的, 線程彼此沒有交互的話極可能看到的不是相同的值。
大多數狀況下,一個線程不關心其餘線程作什麼,若是關心就是synchronization所作的事了。

什麼是不正確的同步(incorrectly synchronization)?

一般狀況下,以下的代碼會被認爲是不正確的同步:

  • 某個線程在寫某個變量
  • 另一個線程在讀該變量
  • 讀和寫並無經過同步進行排序
    這種時候,咱們就說在那個變量上發生了數據競爭(data race),程序也就是一個沒有正確同步的程序

同步(synchronization)作了什麼?

同步有多個方面:最爲人所知的互斥性(mutex): 即同一時間只有一個線程擁有某個對象的監視器(monitor),也意味着一旦一個線程進入了監視器(monitor)對象的同步代碼快中,訪問同一對象的同步代碼塊的其餘線程必須等到擁有監視器(monitor)的線程退出同步代碼庫才行。
可是,synchronization還有一個一個比互斥更重要的特性:同步確保一個線程的內存寫操做對其餘擁有相同監視器(monitor)的線程可見。當咱們推出同步代碼塊時,就釋放了該監視器,它將會把處理器緩存中的數據刷新到內存中,以便於其餘線程能夠看到該線程所作的更改。在咱們進入一個同步代碼塊以前,咱們會申請一個監視器(monitor),這一步驟會使得當前處理器的緩存失效而不得不從內存中從新讀取數據,這樣咱們就能夠看見任意線程所作的任何更改了。
新的內存模型語義在內存操做(read field, write field , lock ,unlock)、線程操做(start , join)中創造了一個非公平順序,即一些操做會發生在另一些操做以前(happen-before),當一個操做在另外一個操做以前時,前面一個將會確保在後面一個的前面而且是可見的。 排序的規則以下:

  • 按照程序代碼中的順序,同一個線程的操做會happens-before以後的操做
  • 同一個監視器上,unloclhappens-before接下來的lock
  • 同一個監視器上,對一個volatile變量的寫操做happens-before讀操做
  • start()方法happens-before調用它的線程的任意操做
  • 線程中的操做happens-before該線程中join()方法返回的全部線程
    上面的規則意味着,對於同一個對象的監視器(monitor),當前同步塊中的內存操做對於以後進入同步代碼的任何線程都是可見的,也就是全部的內存操做happens-before於監視器的釋放,監視器的釋放happens-before監視器的獲取。
    PS: synchronizationreentrant的,也就是同步代碼塊能夠調用同一個監視器的中的其餘同步代碼塊, 也就是說擁有屢次lock, 可是其實是同一個鎖

    Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.
    locksync

爲了創建happens-before關係,線程都應該是在相同的監視器(monitor)上。線程A在對象X的同步代碼塊不是happens-before以後線程B在對象Y的同步代碼塊。釋放和申請鎖必須一一對應,不然,就是一個data race

final在新的內存模型(從JDK1.5開始的)是如何工做的?

對象的final屬性在構造函數中設置,一旦一個對象被正確的構造,給final屬性賦的值對於其餘全部線程都是可見的了,無論是否在同步塊中。 另外,這個final屬性所應用的任何對象或者數組也和final屬性自己同樣對全部線程都是最新的。 這意味這對於final屬性,不須要額外的同步代碼便可保證其餘線程也能夠看見最新的值。
那麼什麼是「對象被正確的構造」?
簡單的說,這意味着該對象的this引用沒有「溢出」構造函數中,否則其餘線程可能經過this引用訪問到「初始化一半」了的對象。好比說,不要賦給靜態域、也不要把其餘對象做爲一個回調等等。這些能夠在構造函數完成事後作而不是構造函數中。

  1. classFinalFieldExample{
  2. finalint x;
  3. int y;
  4. staticFinalFieldExample f;
  5. publicFinalFieldExample(){
  6. x =3;
  7. y =4;
  8. }
  9. staticvoid writer(){
  10. f =newFinalFieldExample();
  11. }
  12. staticvoid reader(){
  13. if(f !=null){
  14. int i = f.x;
  15. int j = f.y;
  16. }
  17. }
  18. }

上面代碼描述瞭如何使用final,調用reader方法的線程必定會看到x的值爲3,由於x是final的。而y則不保證必定是4. 若是上述代碼的構造函數是這樣:

  1. publicFinalFieldExample(){// bad!
  2. x =3;
  3. y =4;
  4. //this溢出,應該避免這樣的代碼
  5. global.obj =this;
  6. }

那麼線程將能夠經過global.obj看到this的引用,x的值就不能保證是3.
對於final屬性能夠看到正確的構造函數中賦的值是很是好的特性,若是final屬性自己是一個引用,那麼在其餘任何線程均可以保證final屬性「至少」會看到構造函數中所指定的值,而若是某個線程經過該引用的方法修改了數據, 則不保證全部線程都能看到該值, 爲了確保全部線程均可以看到最新的值,你仍是必須使用synchronization來同步。

final屬性能夠保證能看到對象構造函數值中最後指定的值, 而不是最新的值。
使用JNI來改變final值是未定義行爲
若是final是引用或者數組類型,仍然須要同步synchronization來保證全部線程均可以看見最新的值

volatile

volatile是一個一般用於線程通訊的特殊字段,每次對volatile變量的讀都會讀取全部線程中最新的值。 因此,它一般被設計爲多線程之間的一個標誌性變量,它的值可能會不停的改變。 編譯器和運行時環境都不容許在寄存器中緩存該變量的值,他們必須確保當有對volatile變量的寫時,值直接被寫到主內存中去以便於其餘線程能夠立馬看到這個改變。一樣的道理,讀volatile變量時,也會清除緩存而後從主內存中從新讀取數據。這也致使在volatile變量時會禁止reorder
在JSR133中,
volatile任然是不容許被重排序的,這也使得重排序它周圍的正常變量變得更難,不過這不是多線程程序編寫者關心的問題=_=. 對一個volatile變量的寫操做就相似於釋放一個監視器(monitor),對一個volatile`變量的讀就相似於申請得到一個監視器('monitor')。
好比說:

  1. classVolatileExample{
  2. int x =0;
  3. volatileboolean v =false;
  4. publicvoid writer(){
  5. x =42;
  6. v =true;
  7. }
  8. publicvoid reader(){
  9. if(v ==true){
  10. //x能夠保證是42
  11. }
  12. }
  13. }

考慮一個線程調用write方法一個線程調用reader方法,write方法寫42到主內存中並釋放監視器, read方法申請監視器並從主內存中讀取42.
從上面的介紹能夠看出來volatile的做用很相似於synchronization,均可以保證獲取到最新值, 因此對於volatile變量的讀寫都具備原子性,可是必須注意的是x++這種複合操做或者多個volatile操做是不具備原子性的,也就是結果不定的。


引用:
jsr133-faq
jsr133-synchronization

相關文章
相關標籤/搜索