寫個文章是由於一次字節面試中,問到java內存模型瞭解嗎?我答了一些堆、方法區、虛擬機棧什麼的。而後說這個不是。我一臉矇蔽。。。以後瞭解到JMM,才知道本身有多蠢,原來是這些東西,原來這些叫JMM。java
因此,如今寫一篇文章總結一下。面試
你們都知道java是經過java虛擬機來跨平臺運行。但,它是怎麼實現的呢,有沒有什麼規則?數組
答:不一樣計算機操做系統對內存模型操做不同,這時候就要有統一的規範來完成操做。因此就要經過JAVA內存模型(Java Memory Model,JMM)緩存
!!!下面2,3,4,5,6段落都是有關JAVA內存模型的相關規範或者規則。!!!安全
文章最後作總結。多線程
Java內存模型的主要目的是定義程序中各類變量的訪問規則併發
- 關注在虛擬機中把變量值存儲到內存和從內存中取出變量值這樣的底層細節。
這個段落的目標:針對的是線程之間能夠共享的變量。app
變量根據是否能夠共享劃分爲:線程私有的和線程公有的。函數
- 線程私有:局部變量與方法參數
- 線程公有:實例字段、靜態字段和構成數組對象的元素
Java內存模型規定了全部的變量都存儲在主內存中。每條線程還有本身的工做內存,線程的工做內存中保存了被該線程使用的變量的主內存副本,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的數據。如圖所示:性能
這一部分要和JAVA內存區域做區分。
主內存與工做內存之間具體的交互協議,
Java內存模型中定義瞭如下8種操做來完成。Java虛擬機實現時必須保證下面說起的每一種操做都是原子的、不可再分的。(簡單看看就行)
- lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
- unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
- read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
- load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
- use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
- assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
- store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
- write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。
Java設計團隊,將Java內存模型的操做簡化爲read、write、lock和unlock四種,但這只是語言描述上的等價化簡,Java內存模型的基礎設計並未改變。
volatile定義的變量有兩個特性:
1. 概念:當一條線程修改了這個變量的值,新值對於其餘線程來講是能夠當即得知的。而普通變量並不能作到這一點,普通變量的值在線程間傳遞時均須要經過主內存來完成。
2. 線程安全:
在併發中,並不必定是線程安全。
Java裏面的運算操做(這裏指的是a=b+1,相似這種,不是a=1這樣)符並不是原子操做,這致使volatile變量的運算在併發下同樣是不安全的。
網上有不少利用線程對一個變量加10000次,可是最後結果不是10000*線程數
變量的++操做在字節碼中分解爲三個部分(此處並不嚴謹,表明意思爲分紅多步驟),這樣會致使線程不安全(單獨的讀寫是安全的)。
1. 概念:是指處理器採用了容許將多條指令不按程序規定的順序分開發送給各個相應的電路單元進行處理。但並非說指令任意重排,處理器必須能正確處理指令依賴狀況保障程序能得出正確的執行結果。
注意:在同一個線程的方法執行過程當中沒法感知到指令重排序,可是其實其中的一些執行順序發生了改變但保證結果不變。
2. 禁止指令重排序的例子:單例模式懶漢
class Singleton{
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
複製代碼
代碼解釋:假如沒有volatile修飾,在new Singleton
的時候,對instance
已經賦予了內存空間,可是內存中沒有東西。此時有另外一個線程獲取單例去使用,發現這個內存中沒有對象沒法使用(就是初始化一半),發生了線程安全的問題。
原理解釋:彙編指令中增長lock
修飾
3. 使用原則:
對於上面的Java內存模型要求lock、unlock、read、load、assign、use、store、write這八種操做都具備原子性。
可是對於64位的數據類型(long和double),在模型中特別定義了一條寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,
通過實際測試,在目前主流平臺下商用的64位Java虛擬機中並不會出現非原子性訪問行爲,可是對於32位的Java虛擬機,譬如比較經常使用的32位x86平臺下的HotSpot虛擬機,對long類型的數據確實存在非原子性訪問的風險。
若是Java內存模型中全部的有序性都僅靠volatile和synchronized來完成,那麼有不少操做都將會變得很是囉嗦,可是咱們在編寫Java併發代碼的時候並無察覺到這一點,這是由於Java語言中有一個「先行發生」(Happens-Before)的原則。
Java內存模型下一些自然的先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。存在8中規則:
由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write這六個,咱們大體能夠認爲,基本數據類型的訪問、讀寫都是具有原子性的。
若是應用場景須要一個更大範圍的原子性保證,Java內存模型還提供synchronized關鍵字,所以在synchronized塊之間的操做也具有原子性。
可見性就是指當一個線程修改了共享變量的值時,其餘線程可以當即得知這個修改。
Java程序中自然的有序性能夠總結爲:
前半句是指線程內似表現爲串行的語義,後半句是指指令重排序現象和工做內存與主內存同步延遲現象。
這篇文章參考:《深刻理解Java虛擬機(第3版)》