瞎掰JVM:內存模型

0. 瞎掰

一切都從人類語言可以虛構和想象開始。抽象是爲了什麼,個人感覺是爲了更好的傳播和繼承。我常去問別人你的容器是什麼,你的模型是什麼,其實就是想交流程序跑起來的基礎環境是什麼。如果你還沒有基於非常底層的硬件開發,那麼請接受一個上層程序環境構建的語境世界,恰好你是一個JAVA工作者,那我的建議就是先了解內存模型。

1. 理想內存模型

在這裏插入圖片描述

順序一致性模型是一個被想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有
兩大特性:

I 一個線程中的所有操作必須按照程序的順序來執行。
II 所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。

2.硬件級的內存模型

在這裏插入圖片描述

在併發場景下,該模型下有
一個優化:
CPU執行優化;

兩個問題:
緩存一致性;
Out-Of-Order Execution;

三個特性
原子性,是指在一個操作中,CPU 不可以在中途暫停然後再調度,即不被中斷操作,要不執行完成,要不就不執行。
可見性,是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性,即程序執行的順序按照代碼的先後順序執行。

三個特性用來保證併發安全,可見性對應緩存一致性問題,有序性強調了Out-Of-Order Execution 的解決,原子性比較特殊,指令重排序和CPU執行指令優化可能引起非原子性。非原子性的另一個場景,如32位系統讀寫64位long/double 變量時,分爲了兩次讀寫事務,進程間進行總線裁決勝利方進行讀寫事務完成,未成功獲得兩次連續事務便會破壞原子性。緩存一致性協議由CPU緩存系統實現有 mesi 等,同樣功能但效率低是總線鎖技術,但總線鎖的使用範圍在讀寫事務中也有出現。這裏存在僞共享的問題,影響併發性能性能。JVM或者disrupt 各有優化方案。
增加解釋Out-Of-Order Execution,如指令級並行的重排序,現代處理器採用的指令級並行技術(Instruction-Level Parallelism, ILP)來將多條指令重疊執行。若數據不存在依賴性,處理器可以改變語句對應的機器指令的執行順序

3. JMM 內存模型

在這裏插入圖片描述

JVM屏蔽了操作系統和硬件的實現,對於字節碼的指令執行系統調用就是JVM,線程調度和內存管理由JVM管理, 內存系統就是JVM堆,非堆等內存結構,而JMM是上面單元的抽象關係表達。一個 Java 線程的創建本質上就對應了一個本地線程(native thread)的創建
JMM規定了線程的工作內存和主內存的交互關係,以及線程之間的可見性和程序的執行順序.在發生數據競爭時,JMM 對正確同步的多線程程序的內存一致性做了如下保證,如果正確同步,程序(多個線程間)的執行將具有順序一致性。即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。這裏的同步是指廣義上的同步,包括對常用同步原語(lock,volatile和final)的正確使用.但在細節上,兩個模型具有不同:
I 順序一致性模型保證單線程內的操作會按程序的順序執行,而JMM不保證單線程內的操作會按程序的順序執行
II 順序一致性模型保證所有線程只能看到一致的操作執行順序,而JMM不保證所有線程能看到一致的操作執行順序
III JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性,而順序一致性模型保證對所有的內存讀/寫操作都具有原子性.
留意的是,在JSR -133之前的舊內存模型中,一個64位long/ double型變量的讀/寫操作可以被拆分爲兩個32位的讀/寫操作來執行。從JSR -133內存模型開始(即從JDK5開始),僅僅只允許把一個64位long/ double型變量的寫操作拆分爲兩個32位的寫操作來執行,任意的讀操作在JSR -133中都必須具有原子性(即任意讀操作必須要在單個讀事務中執行)。另外我強調以上規則適用普通64位變量。

同樣該模型對三大特性的描述:

原子性
由Java內存模型來直接保證原子性的變量操作包括read、load、use、assign、store、write這6個動作,雖然存在long和double的特例,但基本可以忽律不計,目前虛擬機基本都對其實現了原子性。如果需要更大範圍的控制,lock和unlock也可以滿足需求。lock和unlock雖然沒有被虛擬機直接開給用戶使用,但是提供了字節碼層次的指令monitorenter和monitorexit對應這兩個操作,對應到java代碼就是synchronized關鍵字,因此在synchronized塊之間的代碼都具有原子性。

可見性
可見性是指一個線程修改了一個變量的值後,其他線程立即可以感知到這個值的修改。正如前面所說,volatile類型的變量在修改後會立即同步給主內存,在使用的時候會從主內存重新讀取,是依賴主內存爲中介來保證多線程下變量對其他線程的可見性的。
除了volatile,synchronized和final也可以實現可見性。synchronized關鍵字是通過unlock之前必須把變量同步回主內存來實現的,final則是在初始化後就不會更改,所以只要在初始化過程中沒有把this指針傳遞出去也能保證對其他線程的可見性。

有序性
有序性從不同的角度來看是不同的。單純單線程來看都是有序的,但到了多線程就會跟我們預想的不一樣。可以這麼說:如果在本線程內部觀察,所有操作都是有序的;如果在一個線程中觀察另一個線程,所有的操作都是無序的。前半句說的就是“線程內表現爲串行的語義”,後半句說的是“指令重排序”現象和主內存與工作內存之間同步存在延遲的現象。
保證有序性的關鍵字有volatile和synchronized,volatile禁止了指令重排序,而synchronized則由“一個變量在同一時刻只能被一個線程對其進行lock操作”來保證

補充幾個概念:

數據依賴性是針對單個處理器中執行的指令序列和單個線程中執行的操作,不同處理器和不同線程之間不做考慮。
I 不管怎麼重排序(編譯器和處理器爲了提高並行度),(單線程)程序的執行結果不能被改變。
II 所有的重排序都不會有數據依賴的操作做重排序,因爲這樣會改變最終的執行結果。

在這裏插入圖片描述
先行發生原則是Java內存模型中定義的兩個操作之間的偏序關係。我的理解是JVM預定義了一些有序的JVM之上底層場景的默認規則以簡化編程。這裏偏序的描述又藉助了可見性,可見性表示了線程之間交互的數據同步,有序性表示了線程之間交互的程序同步。

內存語義
先介紹volatile的內存語義
I 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。
II 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

內存語義總結

I 線程A寫一個volatile變量,實質上是線程A向接下來將要讀這個volatile變量的某個線程發出了(其對共享變量所在修改的)消息。
II 線程B讀一個volatile變量,實質上是線程B接收了之前某個線程發出的(在寫這個volatile變量之前對共享變量所做修改的)消息。
III 線程A寫一個volatile變量,隨後線程B讀這個volatile變量,這個過程實質上是線程A通過主內存向線程B發送消息。

這與緩存一致性協議非常契合。包括volitile 內存的語義使用storestore(控制volitile寫後的重排序)等內存屏障,其在硬件內存結果的對應也是內存屏障。
另一個需要注意的事CAS,CAS同時具有volatie讀和寫的內存語義。在CAS和volatile之上實現了豐富的鎖和concurrent 基礎。鎖的釋放和獲取vlotile的寫和讀具有同樣的內存語義,即通知其他即將讀和寫/獲取和釋放的程序。

最後反本溯源總結一下,爲了保證共享內存的正確性(可見性、有序性、原子性),內存模型定義了共享內存系統中多線程程序讀寫操作行爲的規範。JMM便是基於共享內存實現的內存模型。

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