學習Java併發編程,JMM是繞不過的檻。在Java規範裏面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是爲了可以支多線程程序設計的,每一個線程能夠是和其餘線程在不一樣的CPU核心上運行,或者對於多處理器的機器而言,該模型須要實現的就是使得每個線程就像運行在不一樣的機器、不一樣的CPU或者自己就不一樣的線程上同樣,這種狀況實際上在項目開發中是常見的。簡單來講,就是爲了屏蔽系統和硬件的差別,讓一套代碼在不一樣平臺下能到達相同的訪問結果。 固然你要是想作高性能運算,這個仍是要和硬件直接打交道的,博主以前搞高性能計算,用的通常都是C/C++,更老的語言還有Fortran,不過如今並行計算也是有不少計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等。JMM在設計之初也是有很多缺陷的,不事後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。html
JMM即爲JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。java
注意,此處的變量與Java編程裏面的變量有所不一樣步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數(局部變量和方法參數線程私有的,不會共享,固然不存在數據競爭問題,若是局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,可是reference引用自己在Java棧的局部變量表中,是線程私有的)。爲了得到較高的執行效能,Java內存模型並無限制執行引發使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。編程
JMM中的主內存、工做內存與jJVM中的Java堆、棧、方法區等並非同一個層次的內存劃分,數組
Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:JMM規定了全部的變量都存儲在主內存(Main Memory)中。每一個線程還有本身的工做內存(Working Memory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。緩存
圖:JMM內存模型安全
這上如能夠看見java線程中工做內存是經過cache來和主內存交互的,這是由於計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,因此現代計算機系統都不得不加入一層或多層讀寫速度儘量接近處理器運算速度的高速緩存(cache
)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。 多線程
線程和線程之間想進行數據的交換通常大體要經歷兩大步驟:1.線程1把工做內存1中的更新過的共享變量刷新到主內存中去;2.線程2到主內存中去讀取線程1刷新過的共享變量,而後copy一份到工做內存2中去。(固然具體實現沒有這麼簡單,具體的操做步驟在下文細講)併發
Java內存模型是圍繞着併發編程中原子性、可見性、有序性這三個特徵來創建的,那咱們依次看一下這三個特徵app
定義:可見性是指當多個線程訪問同一個變量時,當一個線程修改了這個變量的值,其餘線程可以當即得到修改的值。框架
實現原理:JMM是經過將在工做內存中的變量修改後的值同步到主內存,在讀取變量前須要從主內存獲取最新值到工做內存中,這種只從主內存的獲取值的方式來實現可見性的 。
存在問題:多線程程序在可見性方面存在問題,這意味着某些線程可能會讀到舊數據,即髒讀。
解決方案
定義: 即程序執行的順序按照代碼的前後順序執行。這個在單一線程中天然能夠保證,可是多線程中就不必定能夠保證。
問題緣由: 首先處理器爲了提升程序運行效率,可能會對目標代碼進行重排序。重排序是對內存訪問操做的一種優化,它能夠在不影響單線程程序正確性的前提下進行必定的調整,進而提升程序的性能。其保證依據是處理器對涉及依賴關係的數據指令不會進行重排序,沒有依賴關係的則可能進行重排序,即一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2以前執行。(PS:並行計算優化中最基本的一項就是去除數據的依賴關係,方法有不少。)可是在多線程中可能會對存在依賴的操做進行重排序,這可能會改變程序的執行結果。
Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯爲字節碼,代碼編譯階段運行;另外一種是動態編譯JIT,會在運行時,動態的將字節碼編譯爲本地機器碼(目標代碼),提升java程序運行速度。一般javac不會進行重排序,而JIT則極可能進行重排序
圖:java編譯
總結:在本線程內觀察,操做都是有序的;若是在一個線程中觀察另一個線程,全部的操做都是無序的。這是由於在多線程中JMM的工做內存和主內存之間存在延遲,並且java會對一些指令進行從新排序。
解決方案
happens-before 原則:java有一個內置的有序規則,無需加同步限制;若是目標代碼能夠從這個原則中推測出來順序,那麼將會對它們進行有序性保障;若是不能推導出來,換句話說不與這些要求相違背,那麼就可能會被重排序,JVM不會對其有序性進行保障。
JMM定義了8種操做來完成主內存與工做內存的交互細節,虛擬機必須保證這8種操做的每個操做都是原子的,不可再分的。(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許例外)
如今咱們模擬一下兩個線程修改數據的操做流程。線程1 讀取主內存中的值oldNum爲1,線程2 讀取主內存中的值oldNum,而後修改值爲2,流程以下
從上圖能夠看出,實際使用中在一種有可能,其餘線程修改完值,線程的Cache尚未同步到主存中,每一個線程中的Cahe中的值副本不同,可能會形成"髒讀"。緩存一致性協議,就是爲了解決這樣的問題還現,(在這以前還有總線鎖機制,可是因爲鎖機制比較消耗性能,最終仍是被逐漸取代了)。它規定每一個線程中的Cache使用的共享變量副本是同樣的,採用的是總線嗅探技術,流程大體以下
當CPU寫數據時,若是發現操做的變量式共享變量,它將通知其餘CPU該變量的緩存行爲無效,因此當其餘CPU須要讀取這個變量的時候,發現本身的緩存行爲無效,那麼就會從主存中從新獲取。
volatile 會在store時加上一個lock寫完主內存後unlock,這樣保證變量在回寫主內存時保證變量不被別的變量修改,並且鎖的粒度比較小,性能較好。
保證了多線程操做下變量的可見性,即某個一個線程修改了被volatile修飾的變量的值,這個被修改變量的新值對其餘線程來講是當即可見的。
線程池中的許多參數都是採用volatile來修飾的 如線程工廠threadFactory,拒絕策略handler,等到任務的超時時間keepAliveTime,keepAliveTime的開關allowCoreThreadTimeOut,核心池大小corePoolSize,最大線程數maximumPoolSize等。由於在線程池中有若干個線程,這些變量必需保持對全部線程的可見性,否則會引發線程池運行錯誤。
對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做(自增操做是三個原子操做組合而成的複合操做)不具備原子性,緣由就是因爲volatile會在store操做時加上lock,其他線程在執行store時,因爲獲取不到鎖而阻塞,會致使當線程對值的修改失效。
底層實現主要是經過彙編的lock的前綴指令,他會鎖定這塊內存區域的緩存(緩存行鎖定)並寫回到主內存,lock前綴指令實際上至關於一個內存屏障(也能夠稱爲內存柵欄),內存屏障會提供3個功能:
JMM模型則是對於JVM對於內存訪問的一種規範,多線程工做內存與主內存之間的交互原則進行了指示,他是獨立於具體物理機器的一種內存存取模型。
對於多線程的數據安全問題,三個方面,原子性、可見性、有序性是三個相互協做的方面,不是說保障了任何一個就萬事大吉了,另外也並不必定是全部的場景都須要所有都保障纔可以線程安全。
參考資料 https://www.cnblogs.com/lewis0077/p/5143268.html 《java併發編程》