Java多線程之Java內存模型

本文首發於公衆號:老胡碼字java

在介紹Java內存模型以前,咱們先介紹一下計算機硬件的內存模型,由於JVM的併發和物理機器的併發很類似,甚至JVM併發操做中不少設計都是由於計算機系統的設計引起的。編程

硬件的內存模型

你們都知道計算機系統處理任務主要是靠處理器(CPU)來進行運算的,而運算中又會涉及到數據,數據在哪呢,數據天然是存儲在計算機內存中,因此處理器在運算過程當中不可避免的會涉及到與內存的讀寫交互,好比讀取運算所需的數據,存儲運算獲得的數據結果等。而處理器的運算速度相比物理內存的讀寫速度要快得多,因此會出現處理器要等待內存數據讀寫結束後才能進行下一步的運算,所以爲了提升計算機的運算速度,如今的計算機系統爲處理器添加了一層讀寫速度儘可能接近處理器的高速緩存來緩解內存與處理器之間的性能差別。這樣在處理任務時將運算須要的數據複製到緩存中,運算結束後再將數據從緩存中同步寫回到內存,這樣處理器在運算時就不須要等待內存數據讀寫結束了。數組

處理器、高速緩存、內存之間的交互關係圖以下:緩存

如上圖所示,在多處理器系統中由於每一個處理器都有本身的高速緩存,因此這就引起了一個新的問題,若是多個處理器的運算任務都涉及同一塊內存區域,就可能致使各自的高速緩存數據不一致,那這個時候從高速緩存寫回主內存的數據以誰爲準呢?這就是引入高速緩存引起的新問題,咱們稱之爲:緩存一致性。安全

爲了解決緩存一致性的問題,現代計算機系統須要各個處理器讀寫緩存時遵循一些協議(MSI、MESI、MOSI、Synapse、Firefly、DragonProtocal,這些都是緩存協議),按照協議來進行讀寫訪問緩存。bash

既然這裏說的是「硬件的內存模型」,那什麼是內存模型呢?多線程

內存模型能夠理解爲在特定的操做協議下,對特定的內存和高速緩存進行讀寫訪問的抽象。不一樣的物理機器,可能有着不一樣的「內存模型」。併發

除了爲處理器增長高速緩存以外,處理器還會對輸入的代碼程序進行亂序執行優化,保證該亂序執行以後的結果和順序執行的結果一致。舉個例子:性能

int a = 1;
int b = 2;
int c = a + b;
複製代碼

上面的這段代碼將第一行和第二行調換順序對最終的結果沒有任何的影響。而處理器在實際運算過程當中爲了優化性能,也會對代碼的執行順序進行相似的調換(保證結果不變的前提下),這種執行順序的調換稱之爲指令重排序,而JVM中也存在相似的指令重排序優化功能。至於爲何指令重排序會優化性能,它是如何優化性能的,這就涉及到彙編指令的知識,我也不懂彙編指令,這裏就不介紹了,有興趣的能夠本身去了解了解。優化

Java內存模型

前面說過不一樣的物理機器,可能有着不一樣的「內存模型」,而Java虛擬機中定義的內存模型能夠屏蔽不一樣的硬件內存模型,這樣就能夠保證Java程序在各個平臺都能達到一致的內存訪問效果,也就是常說的一次編寫處處運行,由於內存模型爲咱們屏蔽掉了不一樣硬件平臺之間的差別。

主內存和工做內存

Java內存模型中規定全部變量都存儲在主內存(虛擬機內存的一部分)中,主要對應Java的堆內存。這裏提到的變量其實是指共享變量,存在線程間競爭的變量,如:實例變量、靜態變量和構成數組對象的元素,而局部變量和方法參數由於是線程私有的,因此不存在線程間共享和競爭關係,因此也就不在前面提到的變量範圍內。

每一個線程有着本身獨有的工做內存,工做內存中保存了被該線程使用到的變量,這些變量來自主內存變量的副本拷貝。線程對變量的全部讀寫操做都必須在工做內存中進行,不能直接讀寫主內存中的變量。而不一樣線程間的工做內存也是獨立的,一個線程沒法訪問其餘線程的工做內存中的變量。

線程工做時,把須要的變量從主內存中拷貝到本身的工做內存,線程運行結束以後再將本身工做內存中的變量寫回到主內存中,而多個線程間對變量的交互只能經過主內存來間接實現。具體的線程、工做內存、主內存的交互關係圖以下:

經過上面的圖和前面的介紹,咱們就很容易明白咱們日常所說的多線程編程時遇到數據狀態不一致的問題是怎麼產生的。例如:線程1和線程2都須要操做主內存中的共享變量A,當線程1已經在工做內存中修改了共享變量A副本的值可是尚未寫回主內存,這時線程2拷貝了主內存中共享變量A到本身的工做內存中,緊接着線程1將本身工做內存中修改過的共享變量A的副本寫回到了主內存,很明顯線程2加載的共享變量A是以前的舊狀態的數據,這樣就產生了數據狀態不一致的問題。

Java內存模型和硬件內存模型的關係

你們看前面的Java內存模型交互圖和硬件內存模型交互圖能夠發現兩種內存模型實際上是很類似的,實際上Java程序在運行過程當中,最終仍是會映射到具體的硬件處理器內核上,但java內存模型和硬件的內存模型並不徹底一致。

對於硬件內存來講只有寄存器、高速緩存、主內存的概念,並無工做內存(線程私有數據區)和主內存(JVM堆內存)之分,它們只是java內存模型的一種抽象概念並非實際存在的,所以java內存模型對內存的劃分對硬件內存並無任何影響。

在java內存模型中,不管是工做內存仍是主內存,它們都有可能存儲到硬件的主內存、高速緩存或者是寄存器中,因此java內存模型和硬件內存模型是是一種抽象概念和真實物理硬件的交叉關係。關係圖以下:

內存交互

前面說到工做內存與主內存會進行數據讀寫交互,這個讀寫交互具體實現細節則是由Java內存模型來控制的,Java內存模型爲主內存和工做內存間的變量拷貝及同步寫回定義了具體的實現協議,該協議主要由8種操做來完成,不一樣虛擬機在實現時必須保證每個基本數據類型的操做都是原子性不可再分的(long,double類型的變量在某些平臺能夠例外,雖然在JVM規範中沒有強制要求long,double類型具備原子性,可是規範建議各JVM實現成具備原子性的,實際上市面上的JVM也基本都實現了原子性),具體8種操做以下:

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

線程、工做內存、主內存對應這8種操做的交互關係圖以下:

按照上面的8種內存交互操做,若是要把一個變量從主內存複製到工做內存,就須要順序的執行read和load操做,而若是要把一個變量從工做內存同步回主內存,則須要順序執行store和write操做,這裏說的是順序執行,而不是連續執行,這也就意味着兩個操做之間能夠插入其餘操做,例如對主內存中的變量1和變量2訪問時,一種可能的順序是read 1, read 2, load 2, load 1。

除此以外,Java內存模型對這8中操做還存在着其餘的約束:

  • 只容許read和load、store和write這兩對操做成對出現。
  • 不容許線程丟棄它的最近的assign操做,即變量在工做內存中改變以後,必須同步回寫到主內存。
  • 不容許線程把沒有通過assign操做的變量,同步回寫到主內存。
  • 一個新的變量只能在主內存中誕生,不容許在工做內存中使用未經初始化的變量,即對一個變量進行use、store操做以前,必須先執行過load、assign操做。
  • 一個變量在同一時刻只能被一條線程執行lock操做,一旦lock成功,能夠被同一線程重複lock屢次,屢次執行lock以後,只有執行相同次數的unlock操做,變量纔會被解鎖。
  • 對一個變量執行lock操做,將會清空工做內存中該變量的值,因此在執行引擎使用這個變量前,須要從新執行load或assign操做對其進行初始化。
  • 對一個變量執行unlock操做以前,必須先把該變量同步回主內存(執行store、write操做)。
  • 若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許unlock一個被其餘線程lock的變量。

Java內存模型的3個特徵

Java內存模型其實一直是圍繞着併發過程當中的如何處理原子性、可見性和有序性這三個特徵創建的。

原子性(Atomicity)

什麼是原子性呢,原子性是指一個操做不可中斷,不可分割,在多線程中就是指一旦一個線程開始執行某個操做,就不能被其餘線程干擾。

Java內存模型直接用來保證原子性變量的操做包括use、read、load、assign、store、write,咱們大體能夠認爲Java基本數據類型的訪問都是原子性的(long,double除外,前面已經介紹過了),若是用戶要操做一個更大的範圍保證原子性,Java內存模型還提供了lock和unlock來知足這種需求,可是這兩種操做沒有直接開放給用戶,而是提供了兩個更高層次的字節碼指令:monitorenter 和 moniterexit,這兩個指令對應到Java代碼中就是synchronized關鍵字,因此synchronized代碼塊之間的操做具備原子性。

可見性(Visibility)

可見性是指當一個線程修改了變量以後,其餘線程能馬上得知這個修改。

Java內存模型經過將變量修改後將新值同步寫回主內存,在讀取前從主內存刷新變量值,因此JVM內存模型是經過主內存做爲傳遞介質來實現可見性的。不管是普通變量仍是volatile修飾的變量都是這樣的,惟一的區別就是volatile變量在被修改以後會馬上寫回主內存,而在讀取時都會從新去主內存讀取最新的值,而普通變量則在被修改後會先存儲在工做內存,以後再從工做內存寫回主內存,而讀的時候則是從工做內存中讀取該變量的副本拷貝。

除了volatile能夠實現可見性以外,synchronized和final關鍵字也能實現可見性。synchronized同步塊的可見性是由於對一個變量執行unlock操做以前,必須將變量的改動寫回主內存來(store、write兩個操做)實現的。而final字段則是由於一旦final字段初始化完成,其餘線程就能夠訪問final字段的值,並且final字段初始化完成以後就再也不可變。

有序性(Ordering)

前面說過處理器在執行運算的時候,會對程序代碼進行亂序執行優化,也叫作重排序優化。一樣的,在JVM中也存在指令重排序優化,這種優化在單線程中是不會存在問題的,但若是這種優化出如今多線程環境中,就可能會出現多線程安全的問題,由於線程1的指令優化可能影響線程2中某個狀態。

Java提供了volatile和synchronized關鍵字來保證線程間操做的有序性。volatile是由於其自己的禁止指令重排序語義來實現的,而synchronized則是由「同一個變量在同一時刻只能有一個線程對其進行lock操做」這條規則來實現的,這也就是synchronized代碼塊對同一個鎖只能串行進入的緣由。

上面介紹了Java內存模型的3中特性,咱們能夠發現synchronized能夠說是萬能的,它能實現Java多線程中的這3大特性,因此這也早就了不少人在遇到多線程併發操做事都是直接使用synchronized完成,但使用synchronized內置鎖會阻塞須要而又沒有獲取該內置鎖的線程,而Java中的線程與操做系統中的原生線程是一一對應的,因此當synchronized內置鎖致使某個線程阻塞後,會致使系統從用戶態切換到內核態執行阻塞操做,這個操做是很是耗時的。

關於Java內存模型就暫時介紹到這裏,接下來的一篇文章會接着介紹更加輕量級的同步實現:volatile關鍵字,同時還會介紹volatile實現中涉及到的內存屏障。



下面是個人我的公衆號,歡迎關注交流
相關文章
相關標籤/搜索