java併發編程——內存模型

1. 併發編程基礎概念

併發——在操做系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行,但任一個時刻點上只有一個程序在處理機上運行——源自百度百科html

在併發編程中,咱們須要處理兩個關鍵問題:線程之間如何通訊線程之間如何同步,後續篇章將圍繞這兩個問題進行介紹。java

  • 線程通訊:是指線程之間以何種機制來交換信息,在命令式編程中,線程之間的通訊機制有兩種:共享內存和消息傳遞
  • 線程同步:是指程序用於控制不一樣線程之間操做發生相對順序的機制。在Java中,能夠經過volatile,synchronized, 鎖等方式實現同步。

本文主要介紹java的通訊機制,剛介紹常見通訊機制主要包括如下兩種方式:程序員

  1. 共享內存:線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。
  2. 消息傳遞:線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。

Java的併發採用的是共享內存模型,Java線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。在java中,全部實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享。編程

Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。數組

2. JMM內存模型

JMM(Java Memory Model)是JVM規範中定義的一種Java內存模型,它的目的是屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓Java程序在各類平臺上到能達到一致的內存訪問效果。 Java內存模型的主要定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。首先簡單說明幾個經常使用名稱定義:緩存

  • 變量:這裏指包括了實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量與方法參數,後者是線程私有的,不會被共享。
  • 主內存:在java中,實例域、靜態域和數組元素是線程之間共享的數據,它們存儲在主內存中。
  • 工做內存:每條線程都有本身的工做內存,線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。

線程、主內存和工做內存之間交互關係

線程、主內存和工做內存的交互關係如上圖所示,和CPU-緩存-內存很相似。 不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成。 最後注意,爲了得到較好的執行性能,Java內存模型並無限制執行引擎使用處理器的寄存器或者高速緩存來提高指令執行速度,也沒有限制編譯器對指令進行重排序。也就是說,在java內存模型中,也會存在緩存一致性問題和指令重排序的問題安全

3. 內存間交互操做

關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做來完成:多線程

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

因此變量讀寫包含如下幾個步驟:併發

  1. 變量從主內存複製到工做內存——順序執行read和load操做
  2. 變量從工做內存同步到主內存——順序執行store和write操做

注意,Java內存模型只要求上述操做必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是能夠插入其餘指令的。 除了定義以上8中原子操做,Java內存模型還規定了上述8種基本操做在執行時必須知足必定的操做規則,例如如不容許read和load單獨出現(即不容許一個變量從主內存中讀取但工做內存不接受),不容許store和write單獨出現(即不容許從工做內存中發起了回寫單主內存不接受),這裏不一一列舉,詳細網上搜索便可。 Java內存模型還定義了volatile型變量的特殊規則(下一節介紹),以上三種規定共同肯定了Java中哪些內存訪問操做是安全的即:jvm

8種原子操做+操做規則+volatile規定=Java中哪些內存訪問操做是安全的

4. volatile型變量規定

當一個變量被定義爲volatile後,將具有兩種特性:

  • 特性一:保證此變量對全部線程的可見性
  • 特性二:禁止指令重排序優化

4.1 volatile可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。 普通的共享變量不能保證可見性,由於普通共享變量被修改以後,何時被寫入主存是不肯定的,當其餘線程去讀取時,此時內存中可能仍是原來的舊值,所以沒法保證可見性。 可是,須要注意的是volatile變量只保證可見性,可是java裏面的運算並不是所有都是原子操做例如++操做,這樣一樣致使volatile修飾變量java運算不安全。 通常不符合如下兩條規則的運算場景中,咱們須要經過加鎖(synchronized或併發包中的鎖)保證變量原子性:

  • 運算結果並不依賴變量的當前值,或者可以確保只有單一的線程修改變量的值(好比++操做不符合依賴當前值)
  • 變量不須要與其餘狀態變量共同參與不變約束

常見的volatile修飾變量的場景是用來做爲開關控制併發:

volatile開關

4.2 禁止指令重排序

重排序:是指「編譯器和處理器」爲了提升性能,而在程序執行時會對程序進行的重排序。大體能夠分爲如下三類:

  • 編譯器優化指令重排,不改變單線程語義的狀況下,從新安排指令執行的順序。
  • 指令級並行重排序,該優化主要是爲了讓程序發揮現代處理器的指令級並行執行能力,前提是這些語句不存在數據依賴。
  • 內存系統重排序,主要發生在處理器讀寫緩衝區,讀寫過程看起來是無序的,但最終結果是有序的 從Java源代碼到最終實際執行的指令序列,會通過下面三種重排序:

實際執行指令序列
以上重排序可能會致使多線程中出現內存可見性問題,針對編譯器重排序JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。 而對於後兩種重排序,JMM的處理器重排序規則會要求java編譯器在生成指令序列時,插入特定類型的內存屏障(memory barriers,intel稱之爲memory fence)指令,經過內存屏障指令來禁止特定類型的處理器重排序(不是全部的處理器重排序都要禁止)。

下面咱們看下jvm如何實現volatile禁止指令重排序的:

  1. volatile變量寫操做,jvm會向處理器發送一條Lock前綴命令,將變量所在的緩存行系會到系統內存。其餘處理器經過嗅探總線上傳播的數據檢測本身的數據是否過時,若是發現過時會置爲無效,再次使用時會從系統內存獲取
  2. Lock前綴命令禁止該指令與以前和以後的讀和寫指令重排序。

最後,關於volatile禁止重排序幾點使用說明:

  • 不會對volatile讀與volatile讀後面的任意內存操做重排序
  • 不會對volatile寫與volatile寫以前的任意內存操做重排序
  • CAS同時具備volatile讀和寫內存的語義,java的CAS使用現代處理器提供的高效級別的原子指令,這些原子指令以原子方式對內存執行讀-改-寫操做,這是多處理器中實現同步的關鍵。

5. JMM內存模型總結

總的來講JMM內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性三個特徵來創建的。下面就三個特徵分別說明:

5.1 原子性

原子性:即一個操做或者多個操做 要麼所有執行而且執行的過程不會被任何因素打斷,要麼就都不執行。 java內存模型的read、load、assign、use、store和write六個操做直接保證原子性,咱們能夠任務基本數據類型訪問讀寫是具備原子性(特殊說明long double64位操做根據jvm實現有關)。 若是場景中須要大範圍的原子性保證,java內存模型提供了lock和unlock操做來知足,對應到java代碼關鍵字便是——synchronized。

5.2 可見性

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。 除了上面介紹的volatile外,java還提供了兩個關鍵字實現可見性,synchronized和final。

  • final的可見性:是指被final修飾的字段在構造器中一旦完成,那麼在其餘線程就能夠看見final字段值
  • synchronized可見性:是指對一個變量執行unlock操做以前,必須先把次變量同步會主內存這條操做規則限制

5.3 有序性

有序性:即程序執行的順序按照代碼的前後順序執行。 java中自然有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另一個線程,全部操做都是無序的。前半句是指「線程內表現爲串行語義」,後半句表示「指令重排」和「工做內存與主內存同步延遲」現象。 java提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,這裏synchronized則是有「同一時刻只容許一條線程對其進行lock操做」這條操做規定獲取的,這個規則決定了同一個鎖的兩個同步塊只能串行進入。

最後,能夠發現synchronized關鍵字能夠同時解決上述三個問題,固然這個須要付出代價就是性能問題。

參考文檔

《深刻理解java虛擬機》——周志明 www.cnblogs.com/dolphin0520…

相關文章
相關標籤/搜索