java線程之間的通信對程序開發人員是完全透明的,內存的可見性問題很容易困擾很多開發人員。本篇博文將揭開java內存模型的神秘面紗,來看看內存模型到底是怎樣的。
并發編程模型的分類
并發編程中需要處理的兩個關鍵問題:線程之間如何通信
線程之間如何同步
所謂通信是指線程之間以何種機制來交換信息,在命令式編程中,線程的通信機制有兩種:共享內存(隱式通信:通過共享程序的公共狀態,讀-寫內存中的公共狀態實現)
消息傳遞(顯示通信:線程間發送消息實現 ,比較典型的就是wait()和notify())
所謂同步,就是控制不同線程間操作發生相對順序的機制:共享內存(同步是顯示的,由程序開發人員顯示的指定某段代碼或者某個方法需要在線程之間互斥執行)。
消息傳遞(同步是隱式的,消息的發送必須在消息接收之前)。
java的并發采用的是共享內存模型,線程之間的通信是隱式執行的,同步需要開發人員顯示進行控制。
JAVA內存模型(JMM)的抽象
JMM把java虛擬機內部劃分為線程棧和堆。邏輯視圖如下:
JMM邏輯視圖.png
java中所有的實例域、靜態域,數組元素都是存儲在堆內存,堆內存在線程之間共享。而對象引用,局部變量、方法參數和異常處理器參數都是存在在棧內存,也就是線程棧中,線程棧中的變量僅對自己可見,對其他線程不可見。不同線程之間的通信由java內存模型(java memory model ,簡稱JMM)控制。JMM的抽象結構圖,如下:
JMM內存模型抽象圖.png
線程之間的共享變量存儲在堆內存,每個線程都有私有的本地內存(線程棧),私有本地內存中存儲了主內存中共享變量的拷貝,本地內存只是JMM的一個抽象概念,并不真實存在。
上圖中線程A要與線程B通信的話,由于線程本地變量的不可見性,首先要將線程A中變量的更改,刷新到主內存中,然后線程B本地私有的共享變量副本失效,從新讀取刷新的新值,才能完成。從上面的描述看,線程A向線程B通信,必須要經過主內存,JMM控制主內存與每個線程的本地變量的交互,來為java程序員提供內存的可見性。
硬件內存架構
軟件最終還要運行在硬件上,看一下現代計算機硬件內存架構的簡單圖示:
硬件模型.png
現在的計算機一般都有兩個或者多個CPU,其中有些還是多核心實現。
每個CPU都包含一系列的寄存器,它們是CPU內內存的基礎。CPU在寄存器上執行操作的速度遠大于在主存上執行的速度。這是因為CPU訪問寄存器的速度遠大于主存。
每個CPU可能還有一個CPU緩存層。實際上,絕大多數的現代CPU都有一定大小的緩存層。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內部寄存器的速度還要慢一點。一些CPU還有多層緩存,但這些對理解Java內存模型如何和內存交互不是那么重要。只要知道CPU中可以有一個緩存層就可以了。
一個計算機還包含一個主存。所有的CPU都可以訪問主存。主存通常比CPU中的緩存大得多。
CPU的高速緩存雖然解決了效率的問題,但是又帶來了一個新的問題:數據一致性。當一個CPU需要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內容讀到它的內部寄存器中,然后在寄存器中執行操作,這樣就不會使CPU直接與內存相連。當CPU需要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,然后在某個時間點將值刷新回主存。
Java內存模型和硬件內存架構之間的橋接
上面已經提到,Java內存模型與硬件內存架構之間存在差異。硬件內存架構沒有區分線程棧和堆。對于硬件,所有的線程棧和堆都分布在主內中。部分線程棧和堆可能有時候會出現在CPU緩存中和CPU內部的寄存器中。如下圖所示:
image.png
Java內存模型的基礎原理
從源代碼到指令序列的重排序
在程序執行時,為了提高程序的執行性能,編譯器和處理器常常會對指令做重排序,換句話說程序的執行順序和程序開發人員編寫的順序可能會存在差異,這是編譯器和處理器對源代碼做了優化。但是JMM的編譯器重排序規則會禁止特定類型的編譯器重排序,對于處理器的重排序,JMM的處理器重排序規則會要求java編譯器在生成指令時,插入特定的內存屏障(Memory Barriers)指令,來禁止特定類型的處理器重排序。換句話說編譯器和處理器的重排序都是可控的。
重排序分為三類:編譯器重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
指令級重排序:現在的處理器都采用了并行技術,可以將多條執行重疊執行,如果不存在數據依賴性,可以改變語句對應機器語句的執行順序。之所以存在數據依賴的語句不做重排序是因為改變順序后將導致執行結果發生變化。
內存系統重排序:由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
JMM屬于語言級的內存模型,它確保在不同的編譯器和不同的處理器平臺上,通過禁止特定類型的編譯器和處理器重排序,為程序員提供一致性的內存可見性保證。
重排序與內存屏障
為了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。內存屏障又稱為內存柵欄,是一個CPU指令:保證特定的操作順序
影響某些數據的內存可見性
例如: volatile關鍵字 就是通過內存屏障實現的。
happens-before
JSP-133(內存模型)使用happens-before來闡述操作之間的內存可見性。在JMM中如果一個操作執行結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關系。兩個操作具有happens-before關系,并不意味著前一個操作必須要在后一個操作之間執行,happens-before僅僅要求前一個操作的執行結果對后一個操作可見。且前一個操作按順序排在第二個操作的前面。
happens-before規則如下:程序順序規則:一個線程中的每個操作,happens-before于線程中任意后續操作
監視器鎖規則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖
volatil變量規則:對一個volatile域的寫,happens-before與任意后續對這個volatile域的讀
傳遞性:如果A happens-before B ,且B happens-before C , 那么A happens-before C .
重排序
數據依賴
如果兩個操作訪問同一個變量,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據的依賴性。寫后讀 a=1;b=a
寫后寫 a=1 ; a =2
讀后寫 a=b ; b =1
上面三種情況,只要重排序兩個操作的執行順序,程序的執行結果就會發生改變。編譯器和處理器可能會對操作做重排序。做重排序時,會遵守數據依賴性,編譯器和處理器不會改變存在數據依賴關系的兩個操作的執行順序,只針對單個處理器和單個線程而言。
as-if-serial語義
它的意思是不管怎么重排序,單線程執行的結果都不會發生變化。為了遵守as-if-serial語義,編譯器和處理器都不會對存在數據依賴的語句執行重排序。
順序一致性
數據競爭與順序一致性保證
當程序未正確同步時,就可能存在數據競爭。在一個線程中寫一個變量
在另一個線程中讀同一個變量
而且讀寫沒有通過同步來排序
順序一致性內存模型
兩大特性:一個線程中所有操作必須按照程序的順序來執行
所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。
同步程序的順序一致性效果
同步程序的順序一致性效果將于程序在順序一致性模型中的執行結果相同。
未同步程序的執行特性
對于未同步或未正確同步的多線程程序,JMM只提供最小安全性。JMM不保證未同步的程序的執行結果與該程序在順序一致性模型中的執行結果一致。
作者:起個名忒難
鏈接:https://www.jianshu.com/p/de47a2e49e5d