JVM運行時內存結構回顧
在JVM相關的介紹中,有說到JAVA運行時的內存結構,簡單回顧下
總體結構以下圖所示,大體分爲五大塊
而對於方法區中的數據,是屬於全部線程共享的數據結構
而對於虛擬機棧中數據結構,則是線程獨有的,被保存在線程私有的內存空間中,因此這部分數據不涉及線程安全的問題
無論是堆仍是棧,他們都是保存在主內存中的
線程堆棧包含正在執行的每一個方法的全部局部變量(調用堆棧上的全部方法)。線程只能訪問它本身的線程堆棧。
由線程建立的局部變量對於建立它的線程之外的全部其餘線程是不可見的。
即便兩個線程正在執行徹底相同的代碼,兩個線程仍將在每一個本身的線程堆棧中建立該代碼的局部變量。所以,每一個線程都有本身的每一個局部變量的版本。
局部變量能夠是基本類型,在這種狀況下,很顯然它徹底保留在線程堆棧上
局部變量也能夠是對象的引用,這種狀況下,局部變量自己仍舊是在線程堆棧上,可是所指向的對象自己倒是在堆中的
很顯然,全部具備對象引用的線程均可以訪問堆上的對象,儘管是多個局部變量(引用),可是其實是同一個對象,因此若是這個對象有成員變量,那麼將會出現數據安全問題。
如上圖所示,兩個線程,localVariable1並 localVariable2兩個局部變量位於不一樣的線程,可是同時指向的是Object3
簡單說,從上面能夠看得出來,在Java中全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享。
對於多線程的線程安全問題,根本在於共享數據的讀寫。
JMM(Java內存模型)
Java 內存模型做爲JVM的一種抽象內存模型,屏蔽掉各類硬件和操做系統的內存差別,達到跨平臺的內存訪問效果。
Java語言規範定義了一個統一的內存管理模型JMM(Java Memory Model)
無論是堆仍是棧,數據都是保存在主存中的,整個的內存,都只是物理內存的一部分,也就是操做系統分配給JVM進程的那一部分
這部份內存按照運行區域的劃分規則進行了區域劃分
運行時內存區域的劃分,能夠簡單理解爲空間的分配,好比一個房間多少平,這邊用於衣帽間,那邊用於臥室,臥室多大,衣帽間多大
而對於內存的訪問,規定Java內存模型分爲主內存,和工做內存;工做內存就是線程私有的部分,主內存是全部的線程所共享的
每條線程本身的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,全部的工做都是在工做內存這個操做臺上,線程並不能直接操做主存,也不能訪問其餘線程的工做內存
你劃分好了區域,好比有的地方用於存放局部變量,有的地方用於存放實例變量,可是這些數據的存取規則是什麼?
換句話說,如何正確有效的進行數據的讀取?顯然光找好地方存是不行的,怎麼存?怎麼讀?怎麼共享?這又是另外的一個很複雜的問題
好比上面的兩個線程對於Object3的數據讀取順序、限制都是什麼樣子的?
因此內存區域的分塊劃分和工做內存與主存的交互訪問是兩個不一樣的維度
文檔以下:
在對JMM進行介紹以前,先回想下計算機對於數據的讀取
數據本質上是存放於主存(最終是存放於磁盤)中的,可是計算卻又是在CPU中,很顯然他們的速度有天壤之別
因此在計算機硬件的發展中,出現了緩存(一級緩存、二級緩存),藉助於緩存與主存進行數據交互,並且現代計算機中已經不只僅只是有一個CPU
一個簡單的示意圖以下
對於訪問速度來講,寄存器--緩存--主存 依次遞減,可是空間卻依次變大
有了緩存,CPU將再也不須要頻繁的直接從主存中讀取數據,性能有了很大程度的提升(固然,若是須要的數據不在緩存中,那麼仍是須要從主存中去讀取數據,是否存在,被稱爲緩存的命中率,顯然,命中率對於CPU效率有很大影響)
在速度提升的同時,很顯然,出現了一個問題:
若是兩個CPU同時對主存中的一個變量x (值爲1)進行處理,假設一個執行x+1 另一個執行x-1
若是其中一個處理後另外一個纔開始讀取,顯然並無什麼問題
可是若是最初緩存中都沒有數據或者說一個CPU處理過程當中還沒來得及將緩存寫入主存,另外一個CPU開始進行處理,那麼最後的結果將會是不肯定的
這個問題被稱爲:緩存一致性問題
因此說:對於多個處理器運算任務都涉及同一塊主存,須要一種協議能夠保障數據的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等
關於緩存一致性的更多信息能夠查閱
百度百科
緩存一致性(Cache Coherency)入門
緩存、緩存算法和緩存框架簡介
總之,多個CPU,你們使用同一個主存,可是各自不一樣的緩存,天然會有不一致的安全問題。
再回到JMM上來,Java Memory Model
網址:
文中有說到:
The Java memory model specifies how the Java virtual machine works with the computer's memory (RAM). The Java virtual machine is a model of a whole computer so this model naturally includes a memory model - AKA the Java memory model.
Java內存模型指定Java虛擬機如何與計算機內存(RAM)一塊兒工做。
Java虛擬機是整個計算機的模型,所以這個模型天然包括一個內存模型——也就是Java內存模型
對於多線程場景下,對於線程私有的數據是本地的,這個不容置疑,可是對於共享數據,前面已經提到,也是「私有的」
由於每一個線程對於共享數據,都會讀取一份拷貝到本地內存中(也是線程私有的內存),全部的工做都是在本地內存這個操做臺上進行的,以下圖所示
這本質就是一種read-modify-write模式,因此必然有線程安全問題的隱患
與計算機硬件對於主存數據的訪問是否是很類似?
須要注意的是,此處的主存並非像前面硬件架構中的主存(RAM),是一個泛指,保存共享數據的地方,多是主存也多是緩存,總之是操做系統提供的服務,在JMM中能夠統一認爲是主存
這裏的本地內存,就好似對於CPU來講的緩存同樣,很顯然,也會有一致性方面的問題
若是兩個線程之間不是串行的,必然對於數據處理後的結果會出現不肯定性
因此JMM規範究竟是什麼?
他其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是這麼讀取數據的
具體的數據是如何設置到上圖中「主存」這個概念中的?本地內存如何具體的與主存進行交互的?這都是操做系統以及JVM底層實現層面的問題
單純的對於多線程編程來講,就不用管什麼RAM、寄存器、緩存一致性等等問題,就只須要知道:
數據分爲兩部分,共享的位於主存,線程局部的位於私有的工做內存,全部的工做都是在工做內存中進行的,也就意味着有「讀取-拷貝-操做-回寫」這樣一個大體的過程
既然人家叫作JVM java虛擬機,天然是五臟俱全,並且若是不能作到統一形式的內存訪問模型,還叫什麼跨平臺?
若是把線程類比爲CPU,工做內存類比寄存器、緩存,主存類比爲RAM
那麼JMM就至關於解決硬件緩存一致性問題的、相似的一種解決Java多線程讀寫共享數據的協議規範
因此說,若是要設計正確的併發程序,瞭解Java內存模型很是重要。Java內存模型指定了不一樣線程如何以及什麼時候能夠看到其餘線程寫入共享變量的值,以及如何在必要時同步對共享變量的訪問
因此再次強調,單純的從多線程編程的角度看,記住下面這張圖就夠了!!!
因此再次強調,單純的從多線程編程的角度看,記住下面這張圖就夠了!!!
每一個線程局部數據本身獨有,共享數據會讀取拷貝一份到工做內存,操做後會回寫到主存
換一個說法,能夠認爲JMM的核心就是用於解決線程安全問題的,而線程安全問題根本就是對於共享數據的操做,因此說JMM對於數據操做的規範要求,本質也就是多線程安全問題的解決方案(緩存一致性也是數據安全的解決方案)
因此說理解了可能出現問題的緣由與場景,就瞭解了線程安全的問題,瞭解了問題,才能理解解決方案,那多線程到底有哪些主要的安全問題呢?
競爭場景
線程安全問題的本質就是共享數據的訪問,沒有共享就沒有安全問題,因此說有時乾脆一個類中都沒有成員變量,也就避免了線程安全問題,可是很顯然,這只是個別場景下適合,若是一味如此,就是因噎廢食了
若是對於數據的訪問是串行的,也不會出現問題,由於不存在競爭,可是很顯然,隨着計算機硬件的升級,多核處理器的出現,併發(並行)是必然,你不能爲了安全就犧牲掉性能,也是一種因噎廢食
因此換一個說法,爲什麼會有線程安全問題?是由於對於共享數據的競爭訪問!
常見的兩種競爭場景
- read-modify-write(讀-改-寫)
- check-then-act(檢查後行動)
read-modify-write(讀-改-寫)
read-modify-write(讀-改-寫)能夠簡單地分爲三個步驟:
很顯然,若是多個線程同時進行,將會出現不可預知的後果,假設兩個線程,A和B,他們的三個步驟爲A1,A2,A3 和 B1,B2,B3
若是按照A1,A2,A3,B1,B2,B3 或者 B1,B2,B3,A1,A2,A3的順序,並不會出現問題
可是若是是交叉進行,好比A1,A2,B1,B2,B3,A3,那麼就會出現問題,B對數據的寫入被覆蓋了!
check-then-act(檢查後行動)
好比
if(x >1){
//do sth....
x--;
}
若是A線程條件知足後,尚未繼續進行,此時B線程開始執行,條件判斷後知足繼續執行,執行後x的值並不知足條件了!
這也是一種常見的線程安全問題
很顯然,單線程狀況下,或者說全部的變量所有都是局部變量的話,不會出現問題,不然就極可能出現問題(線程安全問題並非必然出現的,長時間不出問題也極可能)
對於線程安全的問題主要分爲三類
原子性
原子 Atomic,意指不可分割,也就是做爲一個總體,要麼所有執行,要麼不會執行
對於共享變量訪問的一個操做,若是對於除了當前執行線程之外的任何線程來講,都是不可分割的,那麼就是具備原子性
簡言之,對於別的線程而言,他要麼看到的是該線程尚未執行的狀況,要麼就是看到了線程執行後的狀況,不會出現執行一半的場景,簡言之,其餘線程永遠不會看到中間結果
生活中有一個典型的例子,就是ATM機取款
儘管中間有不少的工做,好比帳戶扣款,ATM吐出鈔票等,可是從取錢的角度來看,對於用戶倒是不可分割的一個過程
要麼,取錢成功了,要麼取款失敗了,對於共享變量也就是帳戶餘額來講,要麼會減小,要麼不變,不會出現錢去了餘額不變或者餘額減小,可是卻沒有看到錢的狀況
既然是原子操做,既然是不可分割的,那麼就是要麼作了,要麼沒作,不會中間被耽擱,最終的結果看起來就好似串行的執行同樣,不會出現線程安全問題
Java中有兩種方式實現原子性
一種是使用鎖機制,鎖具備排他性,也就是說它可以保證一個共享變量在任意一個時刻僅僅被一個線程訪問,這就消除了競爭;
另一種是藉助於處理器提供的專門的CAS指令(compare-and-swap)
在Java中,long和double之外的任何類型的變量的寫操做都是原子操做
也就是基礎類型(byte int short char float boolean)以及引用類型的變量的寫操做都是原子的,由Java語言規範規定,JVM實現
對於long和double,64位長度,若是是在32位機器上,寫操做可能分爲兩個步驟,分別處理高低32位,兩個步驟就打破了原子性,可能出現數據安全問題
有一點須要注意的是,原子操做+原子操做,並不是仍舊是原子操做
好比
a=1;
b=1;
很顯然,都是原子操做,可是在a=1執行後,若是此時另外的線程過來讀取數據,會讀取到a=1,而b倒是沒設置的中間狀態
可見性
在多線程環境下,一個線程對某個共享變量進行更新以後,後續訪問該變量的線程可能沒法馬上讀取到這個更新的結果,甚至永遠也沒法讀取到這個更新的結果。
這就是線程安全問題的另一個表現形式:可見性(Visibility )
若是一個線程對某個共享變量進行更新以後,後續訪問該變量的線程能夠讀取到該更新的結果,那麼就稱這個線程對該共享變量的更新對其餘線程可見,不然就稱這個線程對該共享變量的更新對其餘線程不可見。
簡言之,若是一個線程對共享數據作出了修改,而另外的線程卻並無讀取到最新的結果,這是有問題的
多線程程序在可見性方面存在問題意味着某些線程讀取到了舊數據,一般也是不被但願的
爲何會出現可見性問題?
由於數據本質是要從主存存取的,可是對於線程來講,有了工做內存,這個私有的工做臺,也就是read-modify-write模式
即便線程正確的處理告終果,可是卻沒有及時的被其餘的線程讀取,而別人卻讀取了錯誤的結果(舊數據),這是一個很大的問題
因此此處也能夠看到,若是僅僅是保障原子性,對於線程安全來講,徹底是不夠的(有些場景可能足夠了)
原子性保障了不會讀取到中間結果,要麼是結束要麼是未開始,可是若是操做結束了,這個結果然的就能看到麼?因此還須要可見性的保障
有序性
關於有序性,首先要說下重排序的概念,若是未曾有重排序,那麼也就不涉及這方面的問題了
好比下面兩條語句
a=1;
b=2;
在源代碼中是有順序的,通過編譯後造成指令後,也必然是有順序的
在一個線程中從代碼執行的角度來看,也老是有前後順序的
好比上面兩條語句,a的賦值在前,b的賦值在後,可是實際上,這種順序是沒有保障的
處理器可能並不會徹底按照已經造成的指令(目標代碼)順序執行,這種現象就叫作重排序
爲何要重排序?
重排序是對內存訪問操做的一種優化,他能夠在不影響單線程程序正確性的前提下進行必定的調整,進而提升程序的性能
可是對於多線程場景下,就可能產生必定的問題
固然,重排序致使的問題,也不是必然出現的
好比,編譯器進行編譯時,處理器進行執行時,都有可能發生重排序
先聲明幾個概念
- 源代碼順序,很明顯字面意思就是源代碼的順序
- 程序順序,源碼通過處理後的目標代碼順序(解釋後或者JIT編譯後的目標代碼或者乾脆理解成源代碼解析後的機器指令)
- 執行順序,處理器對目標代碼執行時的順序
- 感知順序,處理器執行了,可是別人看到的並不必定就是你執行的順序,由於操做後的數據涉及到數據的回寫,可能會通過寄存器、緩存等,即便你先計算的a後計算的b,若是b先被寫回呢?這就是感知順序,簡單說就是別人看到的結果
在此基礎上,能夠將重排序能夠分爲兩種,指令重排序和存儲重排序
下圖來自《Java多線程編程實戰指南-核心篇》
編譯器可能致使目標代碼與源代碼順序不一致;即時編譯器JIT和處理器可能致使執行順序與程序順序不一致;
緩存、緩衝器可能致使感知順序不一致
指令重排序
無論是程序順序與源代碼順序不一致仍是執行順序與程序順序不一致,結果都是指令重排序,由於最終的效果就是源代碼與最終被執行的指令順序不一致
以下圖所示,無論是哪一段順序被重拍了,最終的結果都是最終執行的指令亂序了
ps:Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯爲字節碼,代碼編譯階段運行;JIT是在運行時,動態的將字節碼編譯爲本地機器碼(目標代碼)
一般javac不會進行重排序,而JIT則極可能進行重排序
此處不對爲何要重排序展開,簡單說就是硬件或者編譯器等爲了可以更好地執行指令,提升性能,所作出的必定程度的優化,重排序也不是隨隨便便的就改變了順序的,它具備必定的規則,叫作貌似串行語義As-if-serial Semantics,也就是從單線程的角度保障不會出現問題,可是對於多線程就可能出現問題。
貌似串行語義的規則主要是對於具備數據依賴關係的數據不會進行重排序,沒有依賴關係的則可能進行重排序
好比下面的三條語句,c=a+b;依賴a和b,因此不會與他們進行重排序,可是a和b沒有依賴關係,就可能發生重排序
a=1;
b=2;
c=a+b;
存儲重排序
爲何會出現執行一種順序,而結果的寫入是另外的一種順序?
前面說過,對於CPU來講並非直接跟主存交互的,由於速度有天壤之別,因此有多級緩存,有讀緩存,其實也有寫緩存
有了緩存,也就意味着這中間就多了一些步驟,那麼就可能即便嚴格按照指令的順序執行,可是從結果上看起來倒是亂序的
指令重排序是一種動做,實際發生了,而存儲重排序則是一種現象,從結果看出來的一種現象,其實自己並無在執行上重拍,可是這也可能引發問題
如何保證順序?
貌似串行語義As-if-serial Semantics,只是保障單線程不會出問題,因此有序性保障,能夠理解爲,將貌似貌似串行語義As-if-serial Semantics擴展到多線程,在多線程中也不會出現問題
換句話說,有序性的保障,就是貌似串行語義在邏輯上看起來,有些必要的地方禁止重排序
從底層的角度來看,是藉助於處理器提供的相關指令內存屏障來實現的
對於Java語言自己來講,Java已經幫咱們與底層打交道,咱們不會直接接觸內存屏障指令,java提供的關鍵字synchronized和volatile,能夠達到這個效果,保障有序性(藉助於顯式鎖Lock也是同樣的,Lock邏輯與synchronized一致)
happens-before 原則
關鍵字volatile和synchronized均可以保證有序性,他們都會告知底層,相關的處理須要保障有序,可是很顯然,若是全部的處理都須要主動地去借助於這兩個關鍵字去維護有序,這將是一件繁瑣痛苦的事情,並且,也說到了重排序也並非隨意的
Java有一個內置的有序規則,也就是說,對於重排序有一個內置的規則實現,你不須要本身去動腦子思考,動手去寫代碼,有一些有序的保障Java自然存在,簡化了你對重排序的設計與思考
這個規則就叫作happens-before 原則
若是能夠從這個原則中推測出來順序,那麼將會對他們進行有序性保障;若是不能推導出來,換句話說不與這些要求相違背,那麼就可能會被重排序,JVM不會對有序性進行保障。
程序次序規則(Program Order Rule)
在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構,只要確保在一個線程內最終的結果和代碼順序執行的結果一致便可,仍舊可能發生重排序,可是得保證這個前提
管程鎖定規則(Monitor Lock Rule)
一個unlock操做先行發生於後面對同一個鎖的 lock操做。這裏必須強調的是同一個鎖,而「後面」是指時間上的前後順序
volatile變量規則(Volatile Variable Rule)
對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。
線程啓動規則(Thread Start Rule)
Thread對象的start()方法先行發生於此線程的每個動做。你必須得先啓動一個線程纔能有後續
線程終止規則(Thread Termination Rule)
線程中的全部操做都先行發生於對此線程的終止檢測,也就是說全部的操做確定是要在線程終止以前的,終止以後就不能有操做了,能夠經過Thread.join()方法結束、Thread. isAlive()的返回值等手段檢測到線程已經終止執行。
線程中斷規則(Thread Interruption Rule)
對線程 interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,也就是你得先調用方法,纔會產生中斷,你不能別人發現中斷信號了,你居然你都還沒調用interrupt方法,能夠經過Thread.isinterrupted ()方法檢測到是否有中斷髮生。
對象終結規則(Finalizer Rule)
一個對象的初始化完成(構造函數執行結束)先行發生於它的finalizeO方法的開始,先生後死,這個是必須的
傳遞性(Transitivity)
若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
再次強調:對於happens-before規則,不須要作任何的同步限制,Java是自然支持的
《深刻理解Java虛擬機:JVM高級特性與最佳實踐》中有一個例子對於理解該原則有所幫助
private int value = 0; html
public int getValue() { java
return value; 程序員
} 算法
public void setValue(int value) { 編程
this.value = value; 數組
}
緩存
假設兩個線程A和B,線程A先(在時間上先)調用了這個對象的setValue(1),接着線程B調用getValue方法,那麼B的返回值是多少?
對照着hp原則
不是同一個線程,因此不涉及:程序次序規則
不涉及同步,因此不涉及:管程鎖定規則
沒有volatile關鍵字,因此不涉及:volatile變量規則
沒有線程的啓動,中斷,終止,因此不涉及:線程啓動規則,線程終止規則,線程中斷規則
沒有對象的建立於終結,因此不涉及:對象終結規則
更沒有涉及到傳遞性
因此一條規則都不知足,因此,儘管線程A在時間上與線程B具備前後順序,可是,卻並不涉及hp原則,也就是有序性並不會保障,因此線程B的數據獲取是不安全的!!
好比的確是先執行了,可是沒有及時寫入呢?
簡言之,時間上的前後順序,並不表明真正的先行發生(hp),並且,先行發生(hp)也並不能說明時間上的前後順序是什麼
這也說明,不要被時間前後迷惑,只有真正的有序了,才能保障安全
也就是要麼知足hp原則了(自然就支持有序了),或者藉助於volatile或者synchronized關鍵字或者顯式鎖Lock對他們進行保障(顯式手動控制有序),才能保障有序
happens-before是JMM的一個核心概念,由於對於程序員來講,但願一個簡單高效最重要的是要易用的,易於理解的編程模型,可是反過來講從編譯器和處理器執行的角度來看,天然是但願約束越少越好,沒有約束,那麼就能夠高度優化,很顯然二者是矛盾的,一個但願嚴格、簡單、易用,另外一個則但願儘量少的約束;
happens-before則至關於一個折中後的方案,兩者的一個權衡,以上是基本大體的的一個規範,有興趣的能夠深刻研究happens-before原則
原子性、可見性、有序性
前面說過,原子性保障了要麼執行要麼不執行,不會出現中間結果,可是即便原子了,不可分割了,可是是否對另一個可見,是沒法保障的,因此須要可見性
而有序性則是另外的線程對當前線程執行看起來的順序,因此若是都不可見,何談有序性,因此可見性是有序性的基礎
另外,有序性對於可見性是有影響的,好比某些操做原本在前,結果是可見的,可是重排序後,被排序到了後面,這就可能致使不可見,好比父線程的操做對子線程是可見的,可是若是有些位置順序調整了呢?
總結
Java內存區域的劃分是對於主存的一種劃分,存儲的劃分,而這個主存則是分配給JVM進程的內存空間,而JVM的這部份內存只是物理內存的一部分
這部份內存有共享的主存儲空間,還有一部分是線程私有的本地內存空間
線程所用到的全部的變量都位於線程的本地內存中,局部變量自己就在本地內存,而共享變量則會持有一份私有拷貝
線程的操做臺就是這個本地內存,既不能直接訪問主存也不能訪問其餘線程本地內存,只能藉助於主存進行交互
JMM模型則是對於JVM對於內存訪問的一種規範,多線程工做內存與主內存之間的交互原則進行了指示,他是獨立於具體物理機器的一種內存存取模型
對於多線程的數據安全問題,三個方面,原子性、可見性、有序性是三個相互協做的方面,不是說保障了任何一個就萬事大吉了,另外也並不必定是全部的場景都須要所有都保障纔可以線程安全
好比volatile關鍵字只能保障可見性和有序性以及自身修飾變量的原子性,可是若是是一個代碼段卻並不能保障原子性,因此是一種弱的同步,而synchronized則能夠從三個維度進行保障
這三個特性也是JMM的核心,對相關的原則進行了規範,因此歸納的說什麼是JMM?他就只是一個規範概念
Java經過提供同步機制(synchronized、volatile)關鍵字藉助於編譯器、JVM實現,依賴於底層操做系統,對這些規範進行了實現,提供了對於這些特性的一個保障
反覆提到的下面的這個圖就是JMM的基礎結構,而延展出來的規範特性,就是基於這個結構,而且針對於多線程安全問題提出的一些解決方案
只要正確的使用提供的同步機制,就可以開發出正確的併發程序
如下圖爲結構基礎,定義的線程私有數據空間與主存之間的交互原則