簡介:
Java線程之間的通信對程序員完全透明,內存可見性問題很容易困擾Java程序員,這一系列幾篇文章將揭開Java內存模型的神秘面紗。
這一系列的文章大致分4個部分,分別是:
- Java內存模型基礎,主要介紹內存模型相關基本概念
- Java內存模型中的順序一致性,主要介紹重排序與順序一致性內存模型
-
同步原語,主要介紹三個同步原語(
synchronized
、volatile
和final)的內存語義及重排序規則在處理器中的實現 - Java內存模型的設計,主要介紹Java內存模型的設計原理,及其與處理器內存模型和順序一致性內存模型的關系。
一、Java內存模型的基礎
1.1 并發編程模型的兩個關鍵問題
在并發編程中需要處理兩個關鍵問題:線程之間如何通信及線程之間如何同步(這里的線程是指并發執行的活動實體)。
通信——線程之間以何種機制來交換信息。在命令式編程中,線程之間的通信機制有兩種:共享內存和消息傳遞。
- 共享內存:線程之間共享程序的公共狀態,通過讀寫內存中的公共轉臺進行隱式通信
- 消息傳遞:線程之間沒有公共狀態,線程之間必須通過發送消息來顯式進行通信
同步——程序中用于控制不同線程鍵操作發生相對順序的機制。
- 共享內存:同步是顯式進行的,由于程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執行
- 消息傳遞:同步是隱式進行的,由于消息的發送必須在消息的接收之前。
總結:
Java的并發采用的是共享內存模型,Java線程之間的通信總是隱式進行,整個通信過程對程序員完全透明,如果編寫多線程程序的Java程序員不理解隱式進行線程之間的通信的工作機制,很可能會遇到各種奇怪的內存可見性問題。
1.2 Java內存模型的抽象結構
Java中所有的實例域、靜態域和數組元素都存儲在堆內存中,堆內存在線程之間共享(文章中用“共享變量”指代)。局部變量(Local Variables
)、方法定義參數(Formal Method Parameters
)和異常處理器參數(Exception Handler Parameters
)不會在線程之間共享,它們不會存在內存可見性問題,因此也不受內存模型的影響。
Java線程之間的通信由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存(Local Memory
),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存時JMM的一個抽象概念,并不真實存在。JMM涵蓋了緩存、寫緩沖區、寄存器以及其他的硬件和編譯器優化。
圖示:Java內存模型的抽象示意圖
從上圖來看,線程A和線程B之間要通信的話,必須經歷下面2個步驟。
- 線程A把本地內存A中更新過的變量刷新到主內存中
- 線程B到主內存中去讀取線程A之前已更新過的共享變量
圖示:線程之間通信示意圖
如上圖所示,本地內存A和本地內存B有主內存中共享變量X的副本。假設初始時,這三個內存中的X的值都是0.線程A在執行時,把更新后的X的值(假設值為1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信是,線程A首先把自己本地內存中修改后的X刷新到主內存中,此時主內存中的X值變為了1.隨后,線程B到主內存中去讀取線程A更新后的X值,此時線程B的本地內存X的值也更新成了1。
從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為Java程序員提供內存可見性保證。
1.3 從源代碼到指令重排序
在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分為三種類型:
- 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
-
指令級并行的重排序。現代處理器采用了指令級并行技術(
Instruction-Level Parallelism
,ILP
)來將對跳指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應及其指令的執行順序。 - 內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
從Java源代碼的最終實際執行的指令序列,會分別經歷下面3種重排序,其中1屬于編譯器重排序,2和3屬于處理器重排序。
源代碼到最終執行的指令序列示意圖:
重排序可能會導致多線程程序出現內存可見性問題,對于編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都需要禁止)。對于處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的內存屏障(Memory Barries
, Intel
稱之為Memory Fence
)指令,通過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺之上,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內存可見性保障。
1.4 寫緩沖區和內存屏障
1.4.1 寫緩沖區
現代處理器都會使用寫緩沖區臨時保存向內存中寫入的數據。寫緩沖區的主要作用:
- 可以保證指令流水線持續運行,可以避免由于處理器停頓下來等待向內存寫入數據而產生的延遲。
- 它以批處理的方式方式刷新寫緩沖區,以及合并寫緩沖區中對統一地址的多次寫,減少對內存總線的占用。
常見處理器允許的重排序類型(Y-表示允許兩個操作重排序,N-表示處理器不允許兩個操作重排序)
處理器 \規則 | Load-Load | Load-Store | Store-Store | Store-Load | 數據依賴性 |
---|---|---|---|---|---|
SPARC-TSO | N | N | N | Y | N |
x86 | N | N | N | Y | N |
IA64 | Y | Y | Y | Y | N |
PowerPC | Y | Y | Y | Y | N |
說明:常見處理器都允許Store-Load重排序;常見的處理器都不允許對存在數據依賴性的操作做重排序。N多的表示處理器擁有相對較強的處理器內存模型。
示例項目 \處理器 | Processor A | Processor B |
---|---|---|
偽代碼 | a=1; //A1x=b;//A2 | b=2;//B1y=a;//B2 |
可能運行結果 | 初始狀態:a=b=0;處理器允許執行后得到結果:x=y=0; |
處理器和內存交互:
說明:處理器A和處理器B可以同時把共享變量寫入自己的寫緩沖區(A1、B1),然后從內存中讀取另一個共享變量(A2、B2),最后才把自己寫緩沖區中保存的臟數據刷新到內存中(A3、B3)。當以這種時序執行時,程序就可以得到x=y=0結果。
1.4.2 內存屏障
為了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。
JMM把內存屏障指令分為4類:
屏障類型 | 指令示例 | 說明 |
---|---|---|
LoadLoad Barriers | Load1;LoadLoad;Load2 | 確保Load1數據的裝載先于Load2及所有后續裝載指令的裝載 |
StoreStore Barriers | Store1;StoreStore;Store2 | 確保Store1數據對其他處理器可見(刷新到主內存)先于Store2及所有后續存儲指令的存儲 |
LoadStore Barriers | Load1;LoadStore;Store2 | 確保Load1數據裝載先于Store2及后續的存儲指令刷新到內存 |
StoreLoad Barriers**** | Store1;StoreLoad;Load2 | 確保Store1數據對其他處理器變得可見(指刷新到主內存)先于Load2及所有后續裝載指令的裝載。StoreLoad Barriers會使該屏障之前的所有內存訪問指令(存儲和裝載指令)完成之后,才執行屏障之后的內存訪問指令。 |
StoreLoad Barriers
是一個“全能型屏障”,它同時具有其它3個屏障的效果。現代大多數處理器支持該屏障(其他類型的屏障不一定被所有處理器支持)。執行該屏障開銷會很昂貴,因為處理器需要把緩沖區的內容全部刷新到內存中(Buffer Fully Flush
)。
1.5 happens-before 簡介
從JDK1.5開始,Java使用新的JSR-133內存模型。JSR-133使用happens-before
的概念來闡述操作之間的內存可見性。在JMM中,如果一個操作的結果需要對另一個操作可見,那么這兩個操作之間必須存在happens-before
關系。這里的兩個操作可以是單線程也可以是多線程。
happens-before規則:
-
程序順序規則:一個線程中的每個操作,
happens-before
于該線程的任意后續操作。 -
監視器鎖規則:對于一個鎖的解鎖,
happens-before
于隨后對這個鎖的加鎖。 -
volatile變量規則:對于一個volitale域的寫,
happens-before
于任意后續對這個volatile
域的讀。 -
傳遞性:如果
A happens-before B
,且B happens-before C
,那么A happens-before C
。
注意:
兩個操作之間具有happens-before關系,并不意味著前一個操作必須在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visiable to and ordered beofre the second)。
圖示happens-before與JMM的關系:
一個happens-before
規則對應于一個或多個編譯器個處理器重排序規則。對于Java程序員來說,happens-before
規則簡單易懂,它避免了Java程序員為了理解JMM提供的內存可見性保證而去學習復雜的重排序規則以及這些規則的具體實現方法。
到此這篇關于Java并發編程之內存模型的文章就介紹到這了,更多相關Java內存模型內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/7017977027978330126