Java內存模型(Java Memory Model,JMM)是java虛擬機規範定義的,用來屏蔽掉java程序在各類不一樣的硬件和操做系統對內存的訪問的差別,這樣就能夠實現java程序在各類不一樣的平臺上都能達到內存訪問的一致性。能夠避免像c++等直接使用物理硬件和操做系統的內存模型在不一樣操做系統和硬件平臺下表現不一樣,好比有些c/c++程序可能在windows平臺運行正常,而在linux平臺卻運行有問題。java
物理硬件和內存linux
首先,在單核電腦中,處理問題要簡單的多。對內存和硬件的要求,各類方面的考慮沒有在多核的狀況下複雜。電腦中,CPU的運行計算速度是很是快的,而其餘硬件好比IO,網絡、內存讀取等等,跟cpu的速度比起來是差幾個數量級的。而無論任何操做,幾乎是不可能都在cpu中完成而不借助於任何其餘硬件操做。因此協調cpu和各個硬件之間的速度差別是很是重要的,要否則cpu就一直在等待,浪費資源。而在多核中,不只面臨如上問題,還有若是多個核用到了同一個數據,如何保證數據的一致性、正確性等問題,也是必需要解決的。c++
目前基於高速緩存的存儲交互很好的解決了cpu和內存等其餘硬件之間的速度矛盾,多核狀況下各個處理器(核)都要遵循必定的諸如MSI、MESI等協議來保證內存的各個處理器高速緩存和主內存的數據的一致性。
除了增長高速緩存,爲了使處理器內部運算單元儘量被充分利用,處理器還會對輸入的代碼進行亂序執行(Out-Of-Order Execution)優化,處理器會在亂序執行以後的結果進行重組,保證結果的正確性,也就是保證結果與順序執行的結果一致。可是在真正的執行過程當中,代碼執行的順序並不必定按照代碼的書寫順序來執行,可能和代碼的書寫順序不一樣。windows
Java內存模型數組
雖然java程序全部的運行都是在虛擬機中,涉及到的內存等信息都是虛擬機的一部分,但實際也是物理機的,只不過是虛擬機做爲最外層的容器統一作了處理。虛擬機的內存模型,以及多線程的場景下與物理機的狀況是很類似的,能夠類比參考。緩存
Java內存模型的主要目標是定義程序中變量的訪問規則。即在虛擬機中將變量存儲到主內存或者將變量從主內存取出這樣的底層細節。須要注意的是這裏的變量跟咱們寫java程序中的變量不是徹底等同的。這裏的變量是指實例字段,靜態字段,構成數組對象的元素,可是不包括局部變量和方法參數(由於這是線程私有的)。這裏能夠簡單的認爲主內存是java虛擬機內存區域中的堆,局部變量和方法參數是在虛擬機棧中定義的。可是在堆中的變量若是在多線程中都使用,就涉及到了堆和不一樣虛擬機棧中變量的值的一致性問題了。安全
Java內存模型中涉及到的概念有:微信
這裏須要說明一下:主內存、工做內存與java內存區域中的java堆、虛擬機棧、方法區並非一個層次的內存劃分。這二者是基本上是沒有關係的,上文只是爲了便於理解,作的類比
網絡
工做內存與主內存交互多線程
物理機高速緩存和主內存之間的交互有協議,一樣的,java內存中線程的工做內存和主內存的交互是由java虛擬機定義了以下的8種操做來完成的,每種操做必須是原子性的(double和long類型在某些平臺有例外,參考volatile詳解和非原子性協定)
java虛擬機中主內存和工做內存交互,就是一個變量如何從主內存傳輸到工做內存中,如何把修改後的變量從工做內存同步回主內存。
若是要把一個變量從主內存傳輸到工做內存,那就要順序的執行read和load操做,若是要把一個變量從工做內存回寫到主內存,就要順序的執行store和write操做。對於普通變量,虛擬機只是要求順序的執行,並無要求連續的執行,因此以下也是正確的。對於兩個線程,分別從主內存中讀取變量a和b的值,並不同要read a; load a; read b; load b; 也會出現以下執行順序:read a; read b; load b; load a; (對於volatile修飾的變量會有一些其餘規則,後邊會詳細列出),對於這8中操做,虛擬機也規定了一系列規則,在執行這8中操做的時候必須遵循以下的規則:
固然,最重要的仍是如開始所說,這8個動做必須是原子的,不可分割的。
針對volatile修飾的變量,會有一些特殊規定。
Volatile修飾的變量的特殊規則
關鍵字volatile能夠說是java虛擬機中提供的最輕量級的同步機制。java內存模型對volatile專門定義了一些特殊的訪問規則。這些規則有些晦澀拗口,先列出規則,而後用更加通俗易懂的語言來解釋:
假定T表示一個線程,V和W分別表示兩個volatile修飾的變量,那麼在進行read、load、use、assign、store和write操做的時候須要知足以下規則:
總結上面三條規則,前面兩條能夠歸納爲:Volatile類型的變量保證對全部線程的可見性。第三條爲:*Volatile類型的變量禁止指令重排序優化。
可見性是指當一個線程修改了這個變量的值,新值(修改後的值)對於其餘線程來講是當即能夠得知的。正如上面的前兩條規則規定,volatile類型的變量每次值被修改了就當即同步回主內存,每次使用時就須要從主內存從新讀取值。返回到前面對普通變量的規則中,並無要求這一點,因此普通變量的值是不會當即對全部線程可見的。
誤解 :volatile變量對全部線程是當即可見的,因此對volatile變量的全部修改(寫操做)都馬上能反應到其餘線程中。或者換句話說:volatile變量在各個線程中是一致的,因此基於volatile變量的運算在併發下是線程安全的。
這個觀點的論據是正確的,可是根據論據得出的結論是錯誤的,並不能得出這樣的結論。
volatile的規則,保證了read、load、use的順序和連續行,同理assign、store、write也是順序和連續的。也就是這幾個動做是原子性的,可是對變量的修改,或者對變量的運算,卻不能保證是原子性的。若是對變量的修改是分爲多個步驟的,那麼多個線程同時從主內存拿到的值是最新的,可是通過多步運算後回寫到主內存的值是有可能存在覆蓋狀況發生的。以下代碼的例子:
public class VolatileTest { public static volatile int race = 0; public static void increase() { race++ } private static final int THREADS_COUNT = 20; public void static main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT); for (int = 0; i < THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable(){ @Override public void run() { for (int j = 0; j < 10000; j++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(race); } }
代碼就是對volatile類型的變量啓動了20個線程,每一個線程對變量執行1w次加1操做,若是volatile變量併發操做沒有問題的話,那麼結果應該是輸出20w,可是結果運行的時候每次都是小於20w,這就是由於race++操做不是原子性的,是分多個步驟完成的。假設兩個線程a、b同時取到了主內存的值,是0,這是沒有問題的,在進行++操做的時候假設線程a執行到一半,線程b執行完了,這時線程b當即同步給了主內存,主內存的值爲1,而線程a此時也執行完了,同步給了主內存,此時的值仍然是1,線程b的結果被覆蓋掉了。
普通的變量僅僅會保證在該方法執行的過程當中,全部依賴賦值結果的地方都能獲取到正確的結果,但不能保證變量賦值的操做順序和程序代碼的順序一致。由於在一個線程的方法執行過程當中沒法感知到這一點,這也就是java內存模型中描述的所謂的「線程內部表現爲串行的語義」。
也就是在單線程內部,咱們看到的或者感知到的結果和代碼順序是一致的,即便代碼的執行順序和代碼順序不一致,可是在須要賦值的時候結果也是正確的,因此看起來就是串行的。但實際結果有可能代碼的執行順序和代碼順序是不一致的。這在多線程中就會出現問題。
看下面的僞代碼舉例:
Map configOptions; char[] configText; //volatile類型bianliang volatile boolean initialized = false; //假設如下代碼在線程A中執行 //模擬讀取配置信息,讀取完成後認爲是初始化完成 configOptions = new HashMap(); configText = readConfigFile(fileName); processConfigOptions(configText, configOptions); initialized = true; //假設如下代碼在線程B中執行 //等待initialized爲true後,讀取配置信息進行操做 while ( !initialized) { sleep(); } doSomethingWithConfig();
若是initialiezd是普通變量,沒有被volatile修飾,那麼線程A執行的代碼的修改初始化完成的結果initialized = true就有可能先於以前的三行代碼執行,而此時線程B發現initialized爲true了,就執行doSomethingWithConfig()方法,可是裏面的配置信息都是null的,就會出現問題了。
如今initialized是volatile類型變量,保證禁止代碼重排序優化,那麼就能夠保證initialized = true執行的時候,前邊的三行代碼必定執行完成了,那麼線程B讀取的配置文件信息就是正確的。
跟其餘保證併發安全的工具相比,volatile的性能確實會好一些。在某些狀況下,volatile的同步機制性能要優於鎖(使用synchronized關鍵字或者java.util.concurrent包中的鎖)。可是如今因爲虛擬機對鎖的不斷優化和實行的許多消除動做,很難有一個量化的比較。
與本身相比,就能夠肯定一個原則:volatile變量的讀操做和普通變量的讀操做幾乎沒有差別,可是寫操做會性能差一些,慢一些,由於要在本地代碼中插入許多內存屏障指令來禁止指令重排序,保證處理器不發生代碼亂序執行行爲。
long和double變量的特殊規則
Java內存模型要求對主內存和工做內存交換的八個動做是原子的,正如章節開頭所講,對long和double有一些特殊規則。八個動做中lock、unlock、read、load、use、assign、store、write對待32位的基本數據類型都是原子操做,對待long和double這兩個64位的數據,java虛擬機規範對java內存模型的規定中特別定義了一條相對寬鬆的規則:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,也就是容許虛擬機不保證對64位數據的read、load、store和write這4個動做的操做是原子的。這也就是咱們常說的long和double的非原子性協定(Nonautomic Treatment of double and long Variables)。
併發內存模型的實質
Java內存模型圍繞着併發過程當中如何處理原子性、可見性和順序性這三個特徵來設計的。
原子性(Automicity)
由Java內存模型來直接保證原子性的變量操做包括read、load、use、assign、store、write這6個動做,雖然存在long和double的特例,但基本能夠忽律不計,目前虛擬機基本都對其實現了原子性。若是須要更大範圍的控制,lock和unlock也能夠知足需求。lock和unlock雖然沒有被虛擬機直接開給用戶使用,可是提供了字節碼層次的指令monitorenter和monitorexit對應這兩個操做,對應到java代碼就是synchronized關鍵字,所以在synchronized塊之間的代碼都具備原子性。
可見性
有序性從不一樣的角度來看是不一樣的。單純單線程來看都是有序的,但到了多線程就會跟咱們預想的不同。能夠這麼說:若是在本線程內部觀察,全部操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句說的就是「線程內表現爲串行的語義」,後半句值得是「指令重排序」現象和主內存與工做內存之間同步存在延遲的現象。
保證有序性的關鍵字有volatile和synchronized,volatile禁止了指令重排序,而synchronized則由「一個變量在同一時刻只能被一個線程對其進行lock操做」來保證。
整體來看,synchronized對三種特性都有支持,雖然簡單,可是若是無控制的濫用對性能就會產生較大影響。
先行發生原則
若是Java內存模型中全部的有序性都要依靠volatile和synchronized來實現,那是否是很是繁瑣。Java語言中有一個「先行發生原則」,是判斷數據是否存在競爭、線程是否安全的主要依據。
什麼是先行發生原則
先行發生原則是Java內存模型中定義的兩個操做之間的偏序關係。好比說操做A先行發生於操做B,那麼在B操做發生以前,A操做產生的「影響」都會被操做B感知到。這裏的影響是指修改了內存中的共享變量、發送了消息、調用了方法等。我的以爲更直白一些就是有可能對操做B的結果有影響的都會被B感知到,對B操做的結果沒有影響的是否感知到沒有太大關係。
Java內存模型自帶先行發生原則有哪些
private int value = 0; public void setValue(int value) { this.value = value; } public int getValue() { return this.value; }
若是有兩個線程A和B,A先調用setValue方法,而後B調用getValue方法,那麼B線程執行方法返回的結果是什麼?
咱們去對照先行發生原則一個一個對比。首先是程序次序規則,這裏是多線程,不在一個線程中,不適用;而後是管程鎖定規則,這裏沒有synchronized,天然不會發生lock和unlock,不適用;後面對於線程啓動規則、線程終止規則、線程中斷規則也不適用,這裏與對象終結規則、傳遞性規則也沒有關係。因此說B返回的結果是不肯定的,也就是說在多線程環境下該操做不是線程安全的。
如何修改呢,一個是對get/set方法加入synchronized 關鍵字,可使用管程鎖定規則;要麼對value加volatile修飾,可使用volatile變量規則。
經過上面的例子可知,一個操做時間上先發生並不表明這個操做先行發生,那麼一個操做先行發生是否是表明這個操做在時間上先發生?也不是,以下面的例子:
int i = 2; int j = 1;
在同一個線程內,對i的賦值先行發生於對j賦值的操做,可是代碼重排序優化,也有多是j的賦值先發生,咱們沒法感知到這一變化。
因此,綜上所述,時間前後順序與先行發生原則之間基本沒有太大關係。咱們衡量併發安全的問題的時候不要受到時間前後順序的干擾,一切以先行發生原則爲準。
做者:_fan凡
https://www.jianshu.com/p/15106e9c4bf3
歡迎關注個人微信公衆號「碼農突圍」,分享Python、Java、大數據、機器學習、人工智能等技術,關注碼農技術提高•職場突圍•思惟躍遷,20萬+碼農成長充電第一站,陪有夢想的你一塊兒成長