本節書摘來自異步社群《深入解析android 虛拟機》一書中的第2章,第2.6節,作者:鐘世禮著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視
不同的平台,記憶體模型是不一樣的,但是jvm的記憶體模型規範是統一的。其實java的多線程并發問題最終都會反映在java記憶體模型上,所謂線程安全無非是要控制多個線程對某個資源的有序通路或修改。總結java的記憶體模型,要解決兩個主要的問題:可見性和有序性。
人們都知道計算機有高速緩存的存在,處理器并不是每次處理資料都是取記憶體的。jvm定義了自己的記憶體模型,屏蔽了底層平台記憶體管理細節,對于java開發人員,要清楚在jvm記憶體模型的基礎上,如果解決多線程的可見性和有序性。
那麼,何謂可見性?多個線程之間是不能互相傳遞資料通信的,它們之間的溝通隻能通過共享變量來進行。java記憶體模型(jmm)規定了jvm有主記憶體,主記憶體是多個線程共享的。當建立一個對象的時候,也是被配置設定在主記憶體中,每個線程都有自己的工作記憶體,工作記憶體存儲了主存的某些對象的副本,當然線程的工作記憶體大小是有限制的。當線程操作某個對象時,執行順序如下。
(1)從主存複制變量到目前工作記憶體(read and load)。
(2)執行代碼,改變共享變量值(use and assign)。
(3)用工作記憶體資料重新整理主存相關内容(store and write)。
2.6.1 java記憶體模型概述
java平台自動內建了線程以及多處理器技術,這種內建程度比java以前誕生的計算機語言要厲害很多。該語言針對多種異構平台的平台獨立性而使用的多線程技術支援也是具有開拓性的一面,有時候在開發java同步和線程安全要求很嚴格的程式時,往往容易混淆的一個概念就是記憶體模型。究竟什麼是記憶體模型?記憶體模型描述了程式中各個變量(執行個體域、靜态域和數組元素)之間的關系,以及在實際計算機系統中将變量存儲到記憶體和從記憶體中取出變量這樣的底層細節,對象最終是存儲在記憶體裡面的,這點沒有錯,但是編譯器、運作庫、處理器或者系統緩存可以有特權在變量指定記憶體位置存儲或者取出變量的值。
jvm規範定義了線程對主存的操作指令:read、load、use、assign、store、write。當一個共享變量在多個線程的工作記憶體中都有副本時,如果一個線程修改了這個共享變量,那麼其他線程應該能夠看到這個被修改後的值,這就是多線程的可見性問題。那麼,什麼是有序性呢?線程在引用變量時不能直接從主記憶體中引用,如果線程工作記憶體中沒有該變量,則會從主記憶體中複制一個副本到工作記憶體中,這個過程為read-load,完成後線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中擷取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說read、load、use順序可以由jvm實作系統決定。
2.6.2 主記憶體與工作記憶體
java記憶體模型的主要目标是定義程式中各個變量的通路規則,即在虛拟機中将變量存儲到記憶體和從記憶體中取出變量這樣的底層細節。此處的變量(variable)與java程式設計中所說的變量略有差別,它包括了執行個體字段、靜态字段和構成數組對象的元素,但是不包括局部變量與方法參數,因為後者是線程私有的,不會被共享,自然就不存在競争問題。為了獲得較好的執行效能,java記憶體模型并沒有限制執行引擎使用處理器的特定寄存器或緩存來與主記憶體進行互動,也沒有限制即時編譯器調整代碼執行順序這類權利。
java記憶體模型規定了所有的變量都存儲在主記憶體(main memory)中(此處的主記憶體與介紹實體硬體時的主記憶體名字一樣,兩者也可以互相類比,但此處僅是虛拟機記憶體的一部分)。每條線程還有自己的工作記憶體(working memory,可與前面所講的處理器高速緩存類比),線程的工作記憶體中儲存了被該線程使用到的變量的主記憶體副本複制,線程對變量的所有操作(讀取、指派等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變量。不同的線程之間也無法直接通路對方工作記憶體中的變量,線程間變量值的傳遞均需要通過主記憶體來完成,線程、主記憶體、工作記憶體三者的互動關系如圖2-13所示。
2.6.3 記憶體間互動操作
關于主記憶體與工作記憶體之間具體的互動協定,即一個變量如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實作細節,在java記憶體模型中定義了以下8種操作來完成記憶體間的互動操作。
lock(鎖定):作用于主記憶體的變量,它把一個變量辨別為一條線程獨占的狀态。
unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
read(讀取):作用于主記憶體的變量,它把一個變量的值從主記憶體傳輸到線程的工作記憶體中,以便随後的load動作使用。
load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放人工作記憶體的變量副本中。
use(使用):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳遞給執行引擎,每當虛拟機遇到一個需要使用變量值的位元組碼指令時将會執行這個操作。
assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值賦給工作記憶體的變量,每當虛拟機遇到一個給變量指派的位元組碼指令時執行這個操作。
store(存儲):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳送到主記憶體中,以便随後的write操作使用。
write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中。
如果要把一個變量從主記憶體複制到工作記憶體,那就要按順序地執行read和load操作;如果要把變量從工作記憶體同步回主記憶體,就要按順序地執行store和write操作。注意,java記憶體模型隻要求上述兩個操作必須按順序執行,而沒有保證必須是連續執行。也就是說read與load之間、store與write之間是可插入其他指令的,如對主記憶體中的變量a、b進行通路時,一種可能出現的順序是read a、read b、load b、load a。除此之外,java記憶體模型還規定了在執行上述8種基本操作時必須滿足如下規則。
不允許read和load、store和write操作之一單獨出現,即不允許一個變量從主記憶體讀取了但工作記憶體不接收,或者從工作記憶體發起回寫操作但主記憶體不接收的情況出現。
不允許一個線程丢棄它最近的assign操作,即變量在工作記憶體中改變了之後必須把該變化同步回主記憶體。
不允許一個線程無原因地(沒有發生過任何assign操作)把資料從線程的工作記憶體同步回主記憶體中。
一個新的變量隻能在主記憶體中“誕生”,不允許在工作記憶體中直接使用一個未被初始化(load或assign)的變量,換句話說就是對一個變量實施use和store操作之前,必須先執行過了assign和load操作。
一個變量在同一個時刻隻允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,多次執行lock後,隻有執行相同次數的unlock操作時才會解鎖變量。
如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值。在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。
如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作,也不允許去unlock被其他線程鎖定住的變量。
對一個變量執行unlock操作之前,必須先把此變量同步回主記憶體中(執行store和write操作)。