本文是庫存文章,去年年底學習了慕課網的並發編程課程,今年年初看完了《深入理解Java虛擬機》這本書,但是很多內容忘得差不多了,打算寫寫博客回憶一下那些忘在腦后的知識點。
溫故而知新
更多Java並發文章:https://www.cnblogs.com/hello-shf/category/1619780.html
一、現代計算機內存模型
隨着技術的發展,CPU也在按照摩爾定律快速發展,而內存即主存(Main Memory)發展卻十分緩慢,所以CPU與主存間產生了一種因發展速度帶來的矛盾,CPU發展太快導致主存跟不上CPU的發展速度,所以出現了三級緩存(不一定都是三級,明白就行),一種比主存讀寫速度更高的存儲,三級緩存的出現暫緩了這種矛盾。ok,再遠古的架構我們就先不聊了,從三級緩存的CPU架構看看現代計算機的內存模型。

CPU的流行架構如上圖所示,當CPU要load一個數據時,首先從一級緩存中查找,如果沒有找到再從二級緩存中查找,如果還是沒有就從三級緩存或內存中查找。
在三級緩存架構中我們不難發現,L3 cache和主存是共享的,所以就存在數據一致性的保障,MESI緩存一致性協議就作用在這里。MESI協議規定了CPU從主存(或者三級緩存)加載或者寫入數據的規則,保證了數據的強一致性。關於MESI協議和三級緩存詳情請查閱另一篇博客。
二、Java內存模型—JMM
Java內存模型(Java Memory Model)即JMM是一個抽象的概念,JMM是一個抽象的概念,JMM是一個抽象的概念,並不是物理上的內存划分,重要事情說三遍。
Java內存模型(JMM)定義了Java虛擬機(JVM)在計算機內存(RAM)中的工作規范。在硬件內存模型中,各種CPU架構的實現是不盡相同的,Java作為跨平台的語言,為了屏蔽底層硬件差異,定義了Java內存模型(JMM)。JMM作用於JVM和底層硬件之間,屏蔽了下游不同硬件模型帶來的差異,為上游開發者提供了統一的使用接口。說了這么多其實就是想說明白JMM——JVM——硬件的關系。總之一句話,JMM是JVM的內存使用規范,是一個抽象的概念。

如上圖在JMM中,內存划分為兩個區域,線程本地內存,主內存。
本地內存:每個線程均有自己的本地內存(Local Memory,也稱之為線程的工作內存),本地內存是線程獨占的。
主內存:存儲所有的變量。如果一個變量被多個線程使用(被多個線程load到線程的本地內存中),則該變量被稱之為共享變量。
三、JVM對JMM的實現
依據JMM規范,Java內存模型將JVM分為兩個部分線程棧(Thread Stack)和堆(Heap)。

線程棧:線程獨占,對其他線程不可見。線程間通信或者共享變量需要通過Heap。但另外的線程拿到的也只是該變量的私有拷貝,線程之間不能共享變量本身。
堆:線程創建的對象都在堆區,不管該對象是哪個線程創建的,也不管該對象是成員變量還局部變量。(很好理解,棧是運算速度比較快的區域,對象一般相對較大,棧中只需要一個變量指向堆即可。)
一個誤區

具體各種類型的變量或者對象在JVM中的內存划分不是本文重點,本文我們主要討論JMM。
看上圖,是不是有點疑惑,在JVM中內存不是被划分為Java棧,堆,方法區,本地方法棧程序計數器等區域嗎?為什么兩個圖對應不上呢?原因很簡單,原則上來說這兩個圖是沒有關系的。
JMM和JVM不是一個層次的東西,勉強對應起來,主內存、工作內存從定義上來看,主內存主要對應於java堆中的示例數據部分,而工作內存則對應於虛擬機棧的部分區域,從更低層次來說,主內存就直接對應物理硬件的內存,而為了獲取更好的運行速度,虛擬機(甚至是硬件系統本身的優化措施)可能會讓工作內存優先存儲於寄存器和高速緩存器中,因為程序運行過程中主要讀寫訪問的是工作內存。感覺知乎上有一個不錯的解釋。
四、JMM主內存和本地內存交互操作

計算機硬件內存模型有緩存和主內存的交互協議MESI,同樣JMM也規范了主內存和線程工作內存進行數據交換操作。一共包括如上圖所示的8中操作,並且每個操作都是原子性的。
- lock(鎖定):作用於主內存的變量,一個變量在同一時間只能一個線程鎖定。該操作表示該線程獨占鎖定的變量。
- unlock(解鎖):作用於主內存的變量,表示這個變量的狀態由處於鎖定狀態被釋放,這樣其他線程才能對該變量進行鎖定。
- read(讀取):作用於主內存變量,表示把一個主內存變量的值傳輸到線程的工作內存,以便隨后的load操作使用。
- load(載入):作用於線程的工作內存的變量,表示把read操作從主內存中讀取的變量的值放到工作內存的變量副本中(副本是相對於主內存的變量而言的)。
- use(使用):作用於線程的工作內存中的變量,表示把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時就會執行該操作。
- assign(賦值):作用於線程的工作內存的變量,表示把執行引擎返回的結果賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時就會執行該操作。
- store(存儲):作用於線程的工作內存中的變量,把工作內存中的一個變量的值傳遞給主內存,以便隨后的write操作使用。
- write(寫入):作用於主內存的變量,把store操作從工作內存中得到的變量的值放入主內存的變量中。
JMM規定了以上8中操作需要按照如下規則進行
- 不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
- 不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
- 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
- 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
- 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖。
- 如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
- 如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。
以上8中規則看着也是比較生澀的,其實如果你沒看明白也沒關系,其實這些規則就是保障數據同步的一些規則。不是很重要,重要的在后面的happens-before原則。
五、並發環境下JMM存在的問題
並發編程的三個特征:原子性,有序性,可見性
1 原子性(Atomicity):一個操作是不可中斷的,要么全部執行成功要么全部執行失敗。 2 可見性(Visibility):所有線程都能看到共享內存的最新狀態。(一個線程修改了一個共享變量,其他線程能夠立即看到該變量的最新值) 3 有序性(Ordering):即程序執行的順序按照代碼的先后順序執行。
以上三個概念應該很容易理解,在此不做過多解釋。
1,原子性
JMM保證了四章節中的8個操作是原子性的,Java語言本身對基本數據類型的變量的讀取和賦值操作是原子性操作。(JVM不對double和long類型的變量做原子性保障,可能的原因是緩存行的大小導致的)
比如
x = 1;//1 x ++;//2 y = x;//3
以上三行代碼其實只有x = 1是原子性的,這行代碼只是對x進行賦值。
x ++;不是原子操作,因為這行代碼包含三個操作:加載x的值,執行 ++,然后寫入新值。單個操作是原子性的,多個操作組合起來就不是原子性的了。尤其是在並發環境下,如果x變量是多個線程共享的,會導致線程安全性問題。
y = x;同理也不是原型操作,因為需要首先加載x變量,再賦值給y。
在並發環境下,為了保證原子性通常采用synchronized或者Lock對代碼塊加鎖保證原子性。
2,可見性
在Java中提供了一個volatile關鍵字來保證可見性。當一個主內存中的共享變量被volatile關鍵字修飾時,一個線程對該變量的修改會被立即刷新(store)到主內存,保證其他線程看到的值一定是最新的。可以參考
JMM層面上volatile是通過load/store操作實現的可見性,當然我們也可以通過synchronized和Lock通過加鎖將多線程進行同步也就是串行執行來保證共享變量的可見性。很好理解,當兩個線程都需要操作一個共享變量,后到的線程需要等到先到的線程執行完才能繼續執行,變相的保證了數據的可見性。
當然在可見性層面,加鎖相對於volatile是比較重量級的一個操作。
3,有序性
happens-before原則
1 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於后面的操作。 2 鎖定規則:一個unlock操作先行發生於后面對同一個鎖的lock操作。(先釋放鎖,才能加鎖) 3 volatile變量規則:對同一個變量的寫操作先行發生於后面對這個變量的讀操作。 4 傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於C,則A先行發生於操作C。 5 線程啟動規則:Thread對象的start()方法先行發生於此線程的每一個動作。 6 線程終結規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。 7 線程終結規則:線程中所有的操作都先行發生於線程的終結檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行。 8 對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始。
happens-before是干嘛的呢?我理解的happens-before原則就是一個對人來說顯而易見的東西。但是程序並不能理解這么些東西。
happens-before與可見性
happens-before通過以上8中規則保證可見性,如果一個操作A happens-before 另一個操作B,那么操作A的結果是對操作B可見的。不難理解。
happens-before與重排序
兩個操作如果存在happens-before關系,並不意味着一定是有序進行的,因為JVM存在指令重排優化,如果JVM認為兩個操作重排序有利於性能提升並且重排序后的操作和未重排結果一致,將進行指令重排序。當然JVM層面的重排序發生於編譯期,運行時的指令重排是處理器決定的。
Java語言通過volatile關鍵字通過向主內存加入內存屏障實現禁止指令重排。
如有錯誤的地方還請留言指正。
原創不易,轉載請注明原文地址:https://www.cnblogs.com/hello-shf/p/12091591.html
參考文獻:
https://www.jianshu.com/p/8a58d8335270
http://ifeve.com/java-memory-model-6/
https://www.jianshu.com/p/76959115d486
《深入理解Java虛擬機》
《慕課網-java並發編程》
