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