Java內存模型與線程(一)

Java內存模型與線程

TPS:衡量一個服務性能的標準,每秒事務處理的總數,表示一秒內服務端平均能夠響應的總數,TPS又和併發能力密切相關。

在聊JMM(Java內存模型)之前,先說一下Java爲什麼要定義出JMM,那就要從Java內存模型的作用談起,Java內存模型是用來屏蔽各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。在此之前,C++/C直接使用物理硬件和操作系統的內存模型,因此,會由於平臺或者操作系統的不同,有可能導致在一個平臺上內存訪問正常但是在另一臺併發訪問內存卻會出錯。

在JDK1.5之後,Java內存模型已經成熟和完善起來了。

主內存和工作內存

JMM主要的目標是定義程序中各個變量的訪問規則,這裏的變量包含實例對象,靜態變量,數組但不包含方法上的參數和局部變量,因爲後者是線程私有的,不存在共享問題。

主內存:JMM規定了所有的變量存在主內存中,他這個名字和操作系統中的主存一樣但是意義不一樣,此處指的是虛擬機內存的一部分,兩者的對比圖在下文。
工作內存:線程的工作內存保存了被線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取、賦值等)都只能在工作內存中操作,不能直接在主內存操作變量。不同線程之間無法直接訪問工作內存中的變量,變量的傳遞需要靠主內存完成。
線程、主內存和工作內存之間的關係
在這裏插入圖片描述

處理器、高速緩存、主內存之間的交互關係

在這裏插入圖片描述

內存之間的交互

對於工作內存的變量同步到主內存,主內存的變量的拷貝寫入到工作內存的實現細節,全部採用以下8中原子操作來完成。

  • lock(鎖定):作用於主內存的變量,他把一個變量標識爲一條線程獨佔的狀態。
  • unlock(解鎖):作用於主內存的變量,他把處於鎖定的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
  • read(讀取):作用於主內存的變量,將一個變量的值從主內存中傳輸到線程的工作內存中。以便以後的load操作。
  • load(載入):作用於工作內存的變量,他把read操作從主內存得到的變量值放入工作內存的變量副本中。
  • use(使用):作用於工作內存的變量,他把工作內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用到變量的字節碼指令時就會使用到這個變量。
  • assign(賦值):作用於工作內存的變量,他把一個執行引擎接受到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令的時候就執行這個操作。
  • store(存儲):作用於工作內存的變量,他把工作內存的變量的值傳遞給主內存中,一邊隨後的write操作。
  • write(寫入):作用於主內存的變量,他把store操作從工作內存中得到的變量的值放入主內存的變量中。

如果要把變量從主內存複製到工作內存,就要順序執行read和load操作,如果要把變量從工作內存同步到主內存就要順序執行 store和write操作。注意順序執行的含義是,read要在load之前,但是中間可以進行其他的操作例如read a; read b; load b ;load a; 同樣store和write也是一樣。

以上8中操作必須滿足以下8個規則
1、不允許read和load、store和write操作之一單獨出現。
2、不允許一個線程丟棄他最近的assign操作,也就是變量在工作內存中改變了之後必須把該變化同步回主內存。
3、不允許一個線程無原因(沒有發生任何的assign操作)的將數據從工作內存同步回主內存。
4、不允許新的變量誕生在工作內存換句話說就是store要在load之前,assign要在use之前。
5、一個變量在同一時刻只允許一條線程對其進行lock操作,但是lock可以被同一線程執行多次,多次執行lock後需要執行相同次數的unlock,變量才能被解鎖。
6、如果對一個變量執行lock,那將會清除工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或者assign操作初始化變量的值。
7、如果一個變量事先沒有被lock,那就不允許對他執行unlock,還有不允許unlock一個被其他線程鎖定住的變量。
8、在對一個變量執行unlock之前,必須先把這個變量同步回主內存中(執行store和write)

對於volatile變量的特殊規則

Java內存模型對於volatile專門定義了一些特殊的訪問規則。在沒有volatile之前,一個線程A修改一個變量的值,然後向主內存同步,另一個線程B才能看到這個變量新的值,因此我們可以說這個變量對於線程B可見。
volatile可以保證可見性但是無法保證原子性,所以在併發條件下被volatile修飾的變量也是線程不安全的。

由於volatile變量只能保證可見性,在不符合以下兩個場景中,我們仍要通過加鎖來保證原子性。

  • 運算結果並不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值。
  • 變量不需要與其他的狀態變量共同參與不變約束。

對於volatile變量的第二個語義是進制指令重排優化。普通變量僅僅會保證執行代碼結果的地方都會得到正確的結果,但是不能保證會在執行方法的過程中會按具體操作的執行順序和你寫的代碼的順序一致。在硬件方面上講,CPU允許多條指令不按程序規定的順序分發給各個電路單元處理。但是並不是任意的順序執行,是在保證最後能夠正確的處理指令之間的依賴情況,也就是最後結果是正確的前提下。

那volatile是如何禁止指令重排的呢?
被volatile修飾的變量賦值後多執行了lock 地址 操作,這個操作相當於設置了一個內存屏障(內存屏障的作用就是不能把後面的指令重排序到內存屏障之前的位置),當只有一個CPU訪問內存的時候,不需要內存屏障,但是如果有兩個或更多的CPU訪問同一塊內存,且其中一個在觀測另一個CPU訪問,這時候就需要內存屏障保證一致性。它使得CPU中的緩存寫入到內存,在這個過程中會讓其他CPU或者內核無效化他們自己內存中的被volatile修飾的變量。

對於long和double變量的特殊規則

對於64位的數據類型,在模型中特別定義了 一條相對寬鬆的規定:允許虛擬機在沒有被volatile修飾的long / double類型的變量可以不保證實現原子性(可以劃分爲對其進行兩次32的數據操作),這就是非原子協定。

因爲Java內存模型雖然允許虛擬機不把long和double變量的讀寫操作規定成原子性的,但是虛擬機可以選擇把這些操作實現成原子性的操作,虛擬機有這個選擇的權利,所以在實際開發中,各個平臺的商用虛擬機幾乎都選擇把64位數據的讀寫操作作爲原子性操作來對待,所以一般不需要把long和double變量專門聲明爲volatile。

原子性、可見性、有序性

其實Java內存模型具體圍繞的就是如何處理這三個特性。
原子性:由Java內存模型保證原子性的操作包括read、 load、 assign、 use、 store、 write。我們大致可以認爲這些操作對基本的數據類型都是具備原子性的。如果需要一個大範圍的原子操作,Java內存模型提供了lock和unlock操作滿足這種需求,還提供了更高層次的直接字節碼指令monitorenter和monitorexit來隱式的使用這兩個操作,這兩個字節碼指令反應到Java代碼中就是同步塊—synchronized關鍵字。

可見性:可見性是指當一個線程修改了共享變量的值,其他線程能夠感知這個修改。JMM是通過在變量修改後將新值同步回主存,在變量讀取前從主內存刷新變量值這種依賴主內存作爲傳遞媒介的方式實現可見性的,無論是普通變量還是volatile變量都是如此,但volatile修飾的變量保證了新值能夠立即同步到主內存,以及每次使用前從主內存刷新。因此,可以說volatile保證了多線程操作時變量的可見性,而普通變量不能保證這一點。除volatile外還有synchronized和final也可保證內存的可見性。

有序性:Java提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile關鍵字本身包含了禁止指令重排的語義,而synchronized是:一個變量在同一時刻只允許一條線程對其進行lock操作。這條規則規定了持有同一把鎖的兩個同步塊只能串行執行。天然的有序性在Java中規定的是:如果在本線程觀察的話,所有的操作都是有序的,如果在另外的線程觀察另一個線程,所有的操作都是無序的。

先行先發生原則(happen-before原則)

先行先發生是指:
Java內存模型中定義的兩項操作之間的偏序關係,如果說A先行於B,其實就是說在發生B操作之前,操作A產生的影響能被操作B觀察到,至於這個影響可以是修改內存中的共享變量也可以是發送消息、調用某個方法等。

happen-before要求前一個操作的執行結果對後一個操作可見,並且前一個操作按照順序排在第二個操作之前。

happen-before規則:
  1. 程序次序規則:在一個線程內,按照程序代碼順序,書寫在前面的操作要先行發生於書寫在後面的操作。準確的說,應該是控制流順序而不是程序代碼的順序。
  2. 管程鎖定規則:一個unlock操作要先行發生於lock。這裏需要強調的是通一把鎖。
  3. volatile變量規則:對一個volatile變量的寫操作線性發生於後面對這個變量的讀操作,後面是指時間的先後順序。
  4. 線程啓動規則:Thread對象的start方法先行發生於此線程的每個動作。
  5. 線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測,比如線程A中執行ThreadB.join();name線程B中的任意操作先行於A從ThreadB.join()操作成功返回。
  6. 線程中斷規則:對線程Thread.interrupt方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生。
  7. 對象終結規則:一個對象的初始化完成要先行於他的finalize()方法的開始。
  8. 傳遞性:如果操作A先行發生於操作B,操作B先行發生於操作C,那就可以得出操作A先行發生於C。

時間先後順序與happen-before原則之間基本沒有太大的關係,所以我們衡量併發安全問題的時候不要受到時間順序的干擾,一切以happen-before原則爲準。

參考 :《深入理解Java虛擬機 》周志明

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