併發(JMM綜述)

JMM綜述

一,內存模型產生背景

由於計算機的存儲設備與處理器的運算速度有幾個數量級的差距,爲了避免處理器等待緩慢的內存讀寫操作完成,現代計算機系統通過加入一層讀寫速度儘可能無限接近處理器運算速度的高速緩存

CPU高速缓存

緩存作爲內存和cpu之間的緩衝區,將運算需要用到的數據放入到高速緩存中,讓運算能快速運行,運算結束後在從緩存同步回內存之中

類似redis緩存和數據庫一樣,引入了高速緩存雖然解決了處理器和內存速度的差異,但是卻帶來一個新的問題-----緩存一致性問題

在多核處理器的系統中,每個處理器都有自己的高速緩存,他們共享同一內存,當多個處理器的運算任務都涉及同一塊內存區域時,可能會出現緩存數據不一致的問題,需要使用緩存一致性協議來維護緩存的一致性

缓存一致性

二,內存模型概念

java虛擬機提供java內存模型(JMM)來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果,JMM提出目標在於,定義程序中各個變量的訪問規則,,即在虛擬機中將變量放入內存和從內存中取出變量的過程。

2.1JMM組成部分

  • 共享內存(主內存):存放共享變量
  • 工作內存:是每個線程中的一份私有內存,線程的工作內存不能相互訪問,只能訪問到共享內存,工作內存中,存放的是共享內存中共享變量的一份拷貝

Javaå†å­˜æ¨¡åž‹æŠ½è±¡ç¤ºæ„å›¾

2.2JVM內存操作的併發問題

2.3內存交互操作流程

三,JMM深入

3.1原子性,可見性和有序性

原子性

一個或者多個操作,要麼全部執行並且執行過程中不會被其他因素中斷,要麼全都不執行即使在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所幹擾。

可見性

多個線程訪問一個共享變量時,一個線程修改了該貢獻變量的值,其他線程能立即看到最新被修改後的值,即線程A修改了共享變量值後會立馬寫回到主內存中,其他線程讀取主內存中共享變量的最新值來貢獻本地工作內存中的變量值

有序性

有序性規則表現在以下兩種場景: 線程內和線程間

  • 線程內 從某個線程的角度看方法的執行,指令會按照一種叫“串行”(as-if-serial)的方式執行,此種方式已經應用於順序編程語言。
  • 線程間 這個線程“觀察”到其他線程併發地執行非同步的代碼時,由於指令重排序優化,任何代碼都有可能交叉執行。唯一起作用的約束是:對於同步方法,同步塊(synchronized關鍵字修飾)以及volatile字段的操作仍維持相對有序。

3.2happens-before

happens-before規則是用來描述兩個操作的可見性的,如果A happens-before B,那麼A的結果對B可見

單線程下的 happens-before 字節碼的先後順序天然包含happens-before關係:因爲單線程內共享一份工作內存,不存在數據一致性的問題。 在程序控制流路徑中靠前的字節碼 happens-before 靠後的字節碼,即靠前的字節碼執行完之後操作結果對靠後的字節碼可見。然而,這並不意味着前者一定在後者之前執行。實際上,如果後者不依賴前者的運行結果,那麼它們可能會被重排序。

多線程下的如果沒有實現 happens-before 多線程由於每個線程有共享變量的副本,如果沒有對共享變量做同步處理,線程1更新執行操作A共享變量的值之後,線程2開始執行操作B,此時操作A產生的結果對操作B不一定可見。

所以,在JMM中,如果想要實現一個操作的結果對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係,無論是否在一個線程之內

JMM實現了以下支持happens-before的操作:

  • **程序順序規則:**一個線程中的每個操作,happens-before於該線程的中任意後序操作
  • **監視器鎖規則:**對一個鎖的解鎖,happens-before隨後對一個鎖的加鎖(解鎖後的操作結果對下一次加鎖前可見)
  • **volatile變量規則:**對一個volatile變量的寫happens-before對一個volatile變量的讀(寫入一個volatile變量的結果對下一次volatile變量讀操作可見)
  • **傳遞性:**如果A happens-before B,B happens-before C那麼A happens-before C
  • **線程終止法則:**線程中的任何動作都happens-before與其他線程檢測到這個線程已經終結,或者從Thread.join方法調用中成功返回,或者 Thread.isAlive 方法返回false。
  • **中斷法則法則:**一個線程調用另一個線程的 interrupt 方法 happens-before 於被中斷線程發現中斷(通過拋出InterruptedException, 或者調用 isInterrupted 方法和 interrupted 方法)
  • **終結法則:**一個對象的構造函數的結束 happens-before 於這個對象 finalizer 開始。
  • 線程啓動規則: Thread對象的start()方法 happens-before 此線程的每個一個動作。

JMM爲程序提供了簡單的模型(規則),底層是JMM幫我們程序去實現對於某些類型重排序的禁止

img

3.3內存屏障

Java中如何保證底層操作的有序性和可見性?可以通過內存屏障。這是JMM中另外一個禁止指令重排序的方法

內存屏障是被插入兩個CPU指令之間的一種指令,用來禁止處理器指令發生重排序(像屏障一樣),從而保障有序性的。另外,爲了達到屏障的效果,它也會使處理器寫入、讀取值之前,將主內存的值寫入高速緩存,清空無效隊列(即把失效(產生數據不一致的)的值清除),從而保障可見性

常見的4中屏障

  • LoadLoad屏障: 對於這樣的語句 Load1; LoadLoad; Load2,在Load2及後續讀取操作要讀取的數據被訪問前,保證Load1要讀取的數據被讀取完畢。(Load1和Load2不能發生指令重排序)
  • StoreStore屏障: 對於這樣的語句 Store1; StoreStore; Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。(Store1和Store2不能發生重排序
  • LoadStore屏障: 對於這樣的語句Load1; LoadStore; Store2,在Store2及後續寫入操作被執行前,保證Load1要讀取的數據被讀取完畢。(Load1和store1不能發生重排序
  • StoreLoad屏障: 對於這樣的語句Store1; StoreLoad; Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的(沖刷寫緩衝器,清空無效化隊列)。在大多數處理器的實現中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能。(Store和Load不能發生重排序)

3.3重排序

指令重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重新排序的一種手段

as-if-serial:

不管怎麼重排序,單線程程序執行結果不能被改變

  • 編譯器優化的重排序。編譯器在不改變單線程程序語義(as-if-serial )的前提下,可以重新安排語句的執行順序。
  • 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對機器指令的執行順序。
  • 內存系統的重排序。由於處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。 從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序

img

JMM對程序採取了不同的策略:

  • 對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序
  • 對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不做要求(即允許這種重排序)
    • 在單線程程序中,對於存在控制依賴的操作重排序,不會改變執行結果
    • 但是在多線程程序中,對於存在控制依賴的操作重排序,可能會改變程序執行結果

所以:對於重排序可能會導致的多線程程序出現的內存可見性問題

  • 對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序
  • 對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障指令,通過內存屏障指令來禁止特定類型的處理器重排序

四,volatile

4.1volatile內存語義

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存的共享變量值刷新到主內存中
  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存的共享變量值失效,該線程需要從主內存中讀取共享變量的值

4.2volatile特性

  1. 實現了可見性,保證了不同線程對一個共享變量操作時的可見性,即一個線程修改了共享變量的值會保證這個新值立馬對其他線程可見
  2. 實現了有序性:通過內存屏障禁止了某些類型的指令重排序
  3. 只能保證單條讀或者單條寫命令的原子性,複合操作(i++)不能保證原子性

4.3volatile如何禁止指令重排序

volatile變量的可見性是通過內存屏障實現的,在java編譯器生成的指令序列的適當位置插入內存屏障指令來禁止特定類型的處理器重排序,讓程序按我們預想的流程去執行

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序,確保volatile寫之前的操作不會被重排序到volatile寫之後
  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序,確保volatile讀之後的操作不會被編譯器重排序到讀之前
  • 當第一個是volatile寫,第二個是volatile讀時,都不能進行重排序

所以,需要通過在指令序列中插入內存屏障來保證執行順序

在這裏插入圖片描述

  • 在每個volatile寫操作的後面插入一個StoreLoad屏障。 該屏障除了使volatile寫操作不會與之後的讀操作重排序外,還會刷新處理器緩存,使volatile變量的寫更新對其他線程可見。
  • 在每個volatile讀操作的後面插入一個LoadStore屏障該屏障除了禁止了volatile讀操作與其之後的任何寫操作進行重排序,還會刷新處理器緩存,使其他線程volatile變量的寫更新對volatile讀操作的線程可見。

volatile型变量å†å­˜å±éšœæ’å¥ç­–ç•¥

五,synchronize

通過 synchronized關鍵字包住的代碼區域,對數據的讀寫進行控制:

  • 讀數據 當線程進入到該區域讀取變量信息時,對數據的讀取也不能從工作內存讀取,只能從內存中讀取,保證讀到的是最新的值。
  • 寫數據 在同步區內對變量的寫入操作,在離開同步區時就將當前線程內的數據刷新到內存中,保證更新的數據對其他線程的可見性。

內存語義

  • 當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
  • 當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效,從而使得被監視器保護的臨界區代碼必須從主內存讀取共享變量

六,final

final成員變量必須在聲明的時候初始化或者在構造器中初始化,否則就會報編譯錯誤。 final關鍵字的可見性是指:被final修飾的字段在聲明時或者構造器中,一旦初始化完成,那麼在其他線程無須同步就能正確看見final字段的值。這是因爲一旦初始化完成,final變量的值立刻回寫到主內存。

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