java內存模型

前言

併發處理的廣泛應用是使得Amdahl定律代替摩爾定律成爲計算機性能發展源動力的根本原因,也是人類“壓榨”計算機運算能力的最有力武器。

現代計算機的處理器在追求運算速度的提升的同時,也朝着多核化方向發展。這個趨勢越來越明顯,已經成爲現代處理器發展的主流趨勢。
爲什麼我們希望計算機可以多做一些事呢?
原因是計算機的處理器的處理速度和計算機的存儲設備的讀寫速度相差甚大,我們不希望處理器大部分時間都處於等待狀態,這樣會造成很大資源的浪費。
併發處理能力是衡量一臺服務器好壞的主要指標,而服務端是java語言最擅長的領域之一,所以如何寫好併發程序成爲服務端程序開發的難點之一。下面通過介紹java內存模型來幫助理解java併發的開發。

硬件的效率與一致性

物理機遇到的併發問題與虛擬機中的情況有不少相似之處,物理機對併發的處理方案對於虛擬機的實現也有相當大的參考意義。
由於計算機的存儲設備與處理器的運算速度有幾個數量級的差距,所以現代計算機系統都不得不加入一層讀寫速度儘可能接近處理器運算速度的高速緩存(Cache)來作爲內存與處理器之間的緩衝:將運算需要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。
這樣看似解決了處理器和內存速度的問題,但是仔細一想便會發現,每個cpu都有自己和緩存,那麼他們之間的相同的變量如何保持一致?這個問題也就是緩存一致性問題(Cache Coherence)。每個處理器都有自己的高速緩存,而它們又共享同一主內存(Main Memory)。如圖所示

這裏寫圖片描述

當多個處理器的運算任務都涉及同一塊主內存區域時,將可能導致各自的緩存數據不一致,如果真的發生這種情況,那同步回到主內存時以誰的緩存數據爲準呢?爲了解決一致性的問題,需要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操作,這類協議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。
此外,處理器爲了提高執行效率在運行程序的時候,執行指令的順序並不一定會按照編碼順序來執行,但是會保證結果相同,這種現象稱作亂序。

java內存模型

java虛擬機規範中試圖定義一種java內存模型(java Memory Model,JMM)來屏蔽各種硬件和操作系統之間的差異,以實現讓java程序在這個平臺下都可以達到一致的內存訪問效果。

主存與工作內存

java內存模型的主要目的是定義各個變量的的訪問規則,即在虛擬機中將變量讀取和存儲到內存這樣的底層細節。這裏的變量和java程序中的變量有所區別,它包括了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,因爲後者是線程私有的,不會被共享,自然就不會存在競爭問題。
java內存模型做了如下的規定:

1.所有的變量都存儲在主存中(可以與硬件的主存進行類比,雖然兩者有相似之處,但是這裏提到的主存是JVM中的一部分)
2.每一個線程都有自己的工作內存(可以與cpu cache進行類比),用來保存該線程用到的變量的主存拷貝副本
3.線程對變量的所有操作都只能通過本線程的工作變量進行,不可以直接讀寫主內存中的變量
4.不同的線程無法直接訪問到其他線程的工作內存的變量,需要通過主內進行傳遞

線程、主內存、線程工作內存的關係如圖
這裏寫圖片描述

內存間的交互操作

java定義了以下8中操作來完成變量從主內存拷貝到工作內存、從工作內存同步回主內存。虛擬機實現時必須保證下面提及的每一種操作都是原子的、不可再分的(對於double和long類型的變量來說,load、store、read和write操作在某些平臺上允許有例外)
1.lock(鎖定):作用於主內存的變量,它把一個變量表示爲一個線程獨佔的狀態。
2.unlock(解鎖):作用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
3.read(讀取):作用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用。
4.load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
5.use(使用):作用於工作內存的變量,它把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作。
6.assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
7.store(存儲):作用於工作內存的變量,它把工作內存中一個變量的值傳送到主內存中,以便隨後的write操作使用。
8.write(寫入):作用於主內存的變量,它把store操作從工作內存中得到的變量的值放入主內存的變量中。
java內存模型規定了上述8中操作還要滿足下面的規定:

1.不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
2.不允許一個線程丟棄它的最近的assign操作,即變量在工作內存中改變了之後必須把該變化同步回主內存。
3.不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中。
4.一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操作之前,必須先執行過了assign和load操作。
5.一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量纔會被解鎖。
6.如果對一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
7.如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量。對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)。

對於volatile型變量的特殊規則

關鍵詞volatile可以說是java虛擬機提供的最輕量級的同步機制,它主要有兩個作用:
1.保證此變量對所有的線程都可見
<1>use操作和read、load操作必須是是連續在一起出現,即在執行use操作之間必須先要執行load和read操作。這一點可以理解爲當一個volatile變量在使用之前必須要從主內存中加載最新的值。
<2>assign操作和write、store操作必須是連續出現在一起,即執行assign操作之後必須執行store和write操作。這一點可以理解爲當volatile變量的值被賦值之後就同步回主內存中。
上述的兩點保證了volatile型變量的可見性
2.禁止虛擬機對指令的重排序優化
形成一個內存屏障,指令把修改volatile型變量的值同步到內存時,意味着所有之前的操作都已經執行完成。

這8種內存訪問操作以及上述規則限定,再加上對volatile的一些特殊規定,就已經完全確定了Java程序中哪些內存訪問操作在併發下是安全的。

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