Java併發以內存模型(JMM)淺析

背景

         學習Java併發編程,JMM是繞不過的檻。在Java規範裏面指出了JMM是一個比較開拓性的嘗試,是一種試圖定義一個一致的、跨平臺的內存模型。JMM的最初目的,就是爲了可以支多線程程序設計的,每一個線程能夠是和其餘線程在不一樣的CPU核心上運行,或者對於多處理器的機器而言,該模型須要實現的就是使得每個線程就像運行在不一樣的機器、不一樣的CPU或者自己就不一樣的線程上同樣,這種狀況實際上在項目開發中是常見的。簡單來講,就是爲了屏蔽系統和硬件的差別,讓一套代碼在不一樣平臺下能到達相同的訪問結果。           固然你要是想作高性能運算,這個仍是要和硬件直接打交道的,博主以前搞高性能計算,用的通常都是C/C++,更老的語言還有Fortran,不過如今並行計算也是有不少計算框架和協議的,如MPI協議、基於CPU計算的OpenMp,GPU計算的Cuda、OpenAcc等。JMM在設計之初也是有很多缺陷的,不事後續也逐漸完善起來,還有一個算不上缺陷的缺陷,就是有點難懂。html

什麼是JMM

        JMM即爲JAVA 內存模型(java memory model)。Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節的實現規則。它其實就是JVM內部的內存數據的訪問規則,線程進行共享數據讀寫的一種規則,在JVM內部,多線程就是根據這個規則讀寫數據的。java

         注意,此處的變量與Java編程裏面的變量有所不一樣步,它只是包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量方法參數(局部變量和方法參數線程私有的,不會共享,固然不存在數據競爭問題,若是局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,可是reference引用自己在Java棧的局部變量表中,是線程私有的)。爲了得到較高的執行效能,Java內存模型並無限制執行引發使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。編程

JMM和JVM有什麼區別

  • JVM: Java虛擬機模型 主要描述的是Java虛擬機內部的結構以及各個結構之間的關係,Java虛擬機在執行Java程序的過程當中,會把它管理的內存劃分爲幾個不一樣的數據區域,這些區域都有各自的用途、建立時間、銷燬時間。
  • JMM:Java內存模型 主要規定了一些內存和線程之間的關係,簡單的說就是描述java虛擬機如何與計算機內存(RAM)一塊兒工做。

      JMM中的主內存、工做內存與jJVM中的Java堆、棧、方法區等並非同一個層次的內存劃分,數組

JMM核心知識點

        Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:JMM規定了全部的變量都存儲在主內存(Main Memory)中。每一個線程還有本身的工做內存(Working Memory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。緩存

JMM內存模型

圖:JMM內存模型安全

        這上如能夠看見java線程中工做內存是經過cache來和主內存交互的,這是由於計算機的存儲設備與處理器的運算能力之間有幾個數量級的差距,因此現代計算機系統都不得不加入一層或多層讀寫速度儘量接近處理器運算速度的高速緩存(cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中沒這樣處理器就無需等待緩慢的內存讀寫了。   多線程

  線程和線程之間想進行數據的交換通常大體要經歷兩大步驟:1.線程1把工做內存1中的更新過的共享變量刷新到主內存中去;2.線程2到主內存中去讀取線程1刷新過的共享變量,而後copy一份到工做內存2中去。(固然具體實現沒有這麼簡單,具體的操做步驟在下文細講)併發

1. 三大特徵

        Java內存模型是圍繞着併發編程中原子性、可見性、有序性這三個特徵來創建的,那咱們依次看一下這三個特徵app

 1. 原子性

  • 定義:   一個或者多個操做不能被打斷,要麼所有執行完畢,要麼不執行。在這點上有點相似於事務操做,要麼所有執行成功,要麼回退到執行該操做以前的狀態。
  • 注意點:   通常來講在java中基本類型數據的訪問大都是原子操做,可是對於64位的變量如long 和double類型,在32位JVM中,分別處理高低32位,兩個步驟就打破了原子性,這就致使了long、double類型的變量在32位虛擬機中是非原子操做,數據有可能會被破壞,也就意味着多個線程在併發訪問的時候是線程非安全的。因此如今官方建議最好仍是使用64JVM,64JVM在安全上和性能上都有所提高。
  • 總結:  對於別的線程而言,他要麼看到的是該線程尚未執行的狀況,要麼就是看到了線程執行後的狀況,不會出現執行一半的場景,簡言之,其餘線程永遠不會看到中間結果。
  • 解決方案
    • 鎖機制鎖具備排他性,也就是說它可以保證一個共享變量在任意一個時刻僅僅被一個線程訪問,這就消除了競爭;
    • CAS(compare-and-swap)

2.可見性

      定義:可見性是指當多個線程訪問同一個變量時,當一個線程修改了這個變量的值,其餘線程可以當即得到修改的值。框架

      實現原理:JMM是經過將在工做內存中的變量修改後的值同步到主內存,在讀取變量前須要從主內存獲取最新值到工做內存中,這種只從主內存的獲取值的方式來實現可見性的 。

      存在問題:多線程程序在可見性方面存在問題,這意味着某些線程可能會讀到舊數據,即髒讀。

      解決方案

    • volatile變量:volatile的特殊規則保證了volatile變量值修改後的新值會馬上同步到主內存,因此每次獲取的volatile變量都是主內存中最新的值,所以volatile保證了多線程之間的操做變量的可見性
    • synchronized關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工做內存中(即從主內存中讀取最新值到線程私有的工做內存中),在同步方法/同步塊結束時(Monitor Exit),會將工做內存中的變量值同步到主內存中去(即將線程私有的工做內存中的值寫入到主內存進行同步)。
    • Lock接口的最經常使用的實現ReentrantLock(重入鎖)來實現可見性:當咱們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即便用共享變量時會從主內存中刷新變量值到工做內存中(即從主內存中讀取最新值到線程私有的工做內存中),在方法的最後finally塊裏執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工做內存中的變量值同步到主內存中去(即將線程私有的工做內存中的值寫入到主內存進行同步)。
    • final關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,而且在構造函數中並無把「this」的引用傳遞出去(「this」引用逃逸是很危險的,其餘的線程極可能經過該引用訪問到只「初始化一半」的對象),那麼其餘線程就能夠看到final變量的值。

 3.有序性

        定義: 即程序執行的順序按照代碼的前後順序執行。這個在單一線程中天然能夠保證,可是多線程中就不必定能夠保證。

       問題緣由: 首先處理器爲了提升程序運行效率,可能會對目標代碼進行重排序。重排序是對內存訪問操做的一種優化,它能夠在不影響單線程程序正確性的前提下進行必定的調整,進而提升程序的性能。其保證依據是處理器對涉及依賴關係的數據指令不會進行重排序,沒有依賴關係的則可能進行重排序,即一個指令Instruction 2必須用到Instruction 1的結果,那麼處理器會保證Instruction 1會在Instruction 2以前執行。(PS:並行計算優化中最基本的一項就是去除數據的依賴關係,方法有不少。)可是在多線程中可能會對存在依賴的操做進行重排序,這可能會改變程序的執行結果。

       Java有兩種編譯器,一種是Javac靜態編譯器,將源文件編譯爲字節碼,代碼編譯階段運行;另外一種是動態編譯JIT,會在運行時,動態的將字節碼編譯爲本地機器碼(目標代碼),提升java程序運行速度。一般javac不會進行重排序,而JIT則極可能進行重排序

圖:java編譯

        總結:在本線程內觀察,操做都是有序的;若是在一個線程中觀察另一個線程,全部的操做都是無序的。這是由於在多線程中JMM的工做內存和主內存之間存在延遲,並且java會對一些指令進行從新排序。

        解決方案

    • volatile關鍵字自己經過加入內存屏障來禁止指令的重排序。
    • synchronized關鍵字經過一個變量在同一時間只容許有一個線程對其進行加鎖的規則來實現。
    • happens-before 原則:java有一個內置的有序規則,無需加同步限制;若是目標代碼能夠從這個原則中推測出來順序,那麼將會對它們進行有序性保障;若是不能推導出來,換句話說不與這些要求相違背,那麼就可能會被重排序,JVM不會對其有序性進行保障。

2.八種基本內存交互操做

      JMM定義了8種操做來完成主內存與工做內存的交互細節,虛擬機必須保證這8種操做的每個操做都是原子的,不可再分的。(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許例外)

  • lock (鎖定):做用於主內存的變量,把一個變量標識爲線程獨佔狀態
  • unlock (解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定
  • read (讀取):做用於主內存變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
  • load (載入):做用於工做內存的變量,它把read操做從主存中變量放入工做內存中
  • use (使用):做用於工做內存中的變量,它把工做內存中的變量傳輸給執行引擎,每當虛擬機遇到一個須要使用到變量的值,就會使用到這個指令
  • assign (賦值):做用於工做內存中的變量,它把一個從執行引擎中接受到的值放入工做內存的變量副本中
  • store (存儲):做用於主內存中的變量,它把一個從工做內存中一個變量的值傳送到主內存中,以便後續的write使用
  • write (寫入):做用於主內存中的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中

      如今咱們模擬一下兩個線程修改數據的操做流程。線程1 讀取主內存中的值oldNum爲1,線程2 讀取主內存中的值oldNum,而後修改值爲2,流程以下

       從上圖能夠看出,實際使用中在一種有可能,其餘線程修改完值,線程的Cache尚未同步到主存中,每一個線程中的Cahe中的值副本不同,可能會形成"髒讀"。緩存一致性協議,就是爲了解決這樣的問題還現,(在這以前還有總線鎖機制,可是因爲鎖機制比較消耗性能,最終仍是被逐漸取代了)。它規定每一個線程中的Cache使用的共享變量副本是同樣的,採用的是總線嗅探技術,流程大體以下

       當CPU寫數據時,若是發現操做的變量式共享變量,它將通知其餘CPU該變量的緩存行爲無效,因此當其餘CPU須要讀取這個變量的時候,發現本身的緩存行爲無效,那麼就會從主存中從新獲取。

       volatile 會在store時加上一個lock寫完主內存後unlock,這樣保證變量在回寫主內存時保證變量不被別的變量修改,並且鎖的粒度比較小,性能較好。

3.Volatile關鍵字

  做用

        保證了多線程操做下變量的可見性,即某個一個線程修改了被volatile修飾的變量的值,這個被修改變量的新值對其餘線程來講是當即可見的。

        線程池中的許多參數都是採用volatile來修飾的 如線程工廠threadFactory,拒絕策略handler,等到任務的超時時間keepAliveTime,keepAliveTime的開關allowCoreThreadTimeOut,核心池大小corePoolSize,最大線程數maximumPoolSize等。由於在線程池中有若干個線程,這些變量必需保持對全部線程的可見性,否則會引發線程池運行錯誤。

 缺點

        對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做(自增操做是三個原子操做組合而成的複合操做)不具備原子性,緣由就是因爲volatile會在store操做時加上lock,其他線程在執行store時,因爲獲取不到鎖而阻塞,會致使當線程對值的修改失效。

原理

      底層實現主要是經過彙編的lock的前綴指令,他會鎖定這塊內存區域的緩存(緩存行鎖定)並寫回到主內存,lock前綴指令實際上至關於一個內存屏障(也能夠稱爲內存柵欄),內存屏障會提供3個功能:

  1. 它確保指令重排序時不會把其後面的指令排到內存屏障以前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操做已經所有完成;
  2. 它會強制將對緩存的修改操做當即寫入主存;
  3. 若是是寫操做,它會致使其餘CPU中對應的緩存行無效(MESI緩存一直性協議)。

總結

    JMM模型則是對於JVM對於內存訪問的一種規範,多線程工做內存與主內存之間的交互原則進行了指示,他是獨立於具體物理機器的一種內存存取模型。
對於多線程的數據安全問題,三個方面,原子性、可見性、有序性是三個相互協做的方面,不是說保障了任何一個就萬事大吉了,另外也並不必定是全部的場景都須要所有都保障纔可以線程安全。
參考資料 https://www.cnblogs.com/lewis0077/p/5143268.html 《java併發編程》
相關文章
相關標籤/搜索