在單核計算機中,計算機中的CPU計算速度是很是快的,可是與計算機中的其它硬件(如IO、內存等)同CPU的速度比起來是相差甚遠的,因此協調CPU和各個硬件之間的速度差別是很是重要的,要否則CPU就一直在等待,浪費資源。單核尚且如此,在多核中,這樣的問題會更加的突出。硬件結構以下圖所示:java
咱們先大概梳理下這個流程:當咱們的計算機要執行某個任務或者計算某個數字時,主內存會首先從數據庫中加載計算機計算所須要的數據,由於內存和CPU的速度相差較大,因此有必要在內存和CPU間引入緩存(根據實際的須要,能夠引入多層緩存),主內存中的數據會先存放在CPU緩存中,當這些數據須要同CPU作交互時會加入到CPU寄存器中,最後被CPU使用。程序員
事實上,在單核狀況下,基於緩存的交互能夠很好的解決CPU與其它硬件之間的速度匹配,可是在多核狀況下,各個處理器都要遵循必定的協議來保障內存中的各個處理器的緩存和主內存中的數據一致性問題,這類協議一般被稱爲緩存一致性協議。面試
咱們在開發時會常常遇到這樣的場景,咱們開發完成的代碼在咱們本身的運行環境上表現良好,可是當咱們把它放在其它硬件平臺上時,就會出現各類各樣的錯誤,這是由於在不一樣的硬件生產商和不一樣的操做系統下,內存的訪問邏輯有必定的差別,結果就是當你的代碼在某個系統環境下運行良好,而且線程安全,可是換了個系統就出現各類問題。數據庫
爲了解決這個問題,Java內存模型(JMM)的概念就被提出來了,它的出現能夠屏蔽系統和硬件的差別,讓一套代碼在不一樣平臺下能到達相同的訪問結果,實現平臺的一致性,使得Java程序可以一次編寫,處處運行。編程
這樣的描述的好像有點熟悉啊,這不是JVM的概念描述麼,它們二者有什麼區別啊?segmentfault
JVM與JMM間的區別?緩存
實際上,JMM是Java虛擬機(JVM)在計算機內存(RAM)中的工做方式,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本,本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。而JVM則是描述的是Java虛擬機內部及各個結構間的關係。安全
小夥伴這時可能會有疑問,既然JMM是定義線程和主內存之間的關係,那麼它的出現是否是解決併發領域的問題啊?沒錯,咱們先回顧一下併發領域中的關鍵問題。markdown
併發領域中的關鍵問題?多線程
在編程中,線程之間的通訊機制有兩種,共享內存
和消息傳遞
。
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊,典型的共享內存通訊方式就是經過共享對象進行通訊。
消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊,在java中典型的消息傳遞方式就是wait()和notify()。
同步是指程序用於控制不一樣線程之間操做發生相對順序的機制。 在共享內存併發模型裏,同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。 在消息傳遞的併發模型裏,因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。
事實上,Java內存模型(JMM)的併發採用的是共享內存模型。
下面,咱們一塊兒來學習Java內存模型
咱們先看一張JMM的控制模型做圖
因而可知,Java內存模型(JMM)同CPU緩存模型結構相似,是基於CPU緩存模型來創建的。
咱們先梳理一下JMM的工做流程,以上圖爲例,咱們假設有一臺四核的計算機,cpu1操做線程A,cpu2操做線程B,cpu3操做線程C,當這三個線程都須要對主內存中的共享變量進行操做時,這三條線程分別會將主內存中的共享內存讀入本身的工做內存,本身保存一份共享變量的副本供本身線程自己使用。
這時有的小夥伴可能會有如下疑問:
主內存、工做內存的定義是什麼?
如何將主內存中的共享變量讀入本身線程自己的工做內存?
當其中的某一條線程修改了共享變量後,其他線程中的共享變量值是否變化,若是變化,線程間是怎麼保持可見性的?
下面,咱們針對這兩個疑問一一解答。
主內存主要存儲的是Java實例對象,即全部線程建立的實例對象都存放在主內存中,無論該實例對象是成員變量仍是方法中的本地變量(也稱局部變量),固然也包括了共享的類信息、常量、靜態變量。因爲是共享數據區域,多條線程對同一個變量進行訪問可能會發現線程安全問題。
工做內存主要存儲當前方法的全部本地變量信息(工做內存中存儲着主內存中的變量副本拷貝),即每一個線程只能訪問本身的工做內存,即線程中的本地變量對其它線程是不可見的,就算是兩個線程執行的是同一段代碼,它們也會各自在本身的工做內存中建立屬於當前線程的本地變量,固然也包括了字節碼行號指示器、相關Native方法的信息。注意因爲工做內存是每一個線程的私有數據,線程間沒法相互訪問工做內存,所以存儲在工做內存的數據不存在線程安全問題。
**NOTE:**這裏的主內存、工做內存與Java內存區域中的Java堆、棧、方法區不是同一層次的內存劃分,這二者基本上沒有關係。
搞清楚主內存和工做內存後,下一步就須要學習主內存與工做內存的數據交互操做的方式。
主內存與工做內存的交互操做有8種,虛擬機必須保證每個操做都是原子的,這八種操做分別是:
做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定
做用於主內存變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
做用於工做內存的變量,它把read操做從主存中變量放入工做內存中
做用於工做內存中的變量,它把工做內存中的變量傳輸給執行引擎,每當虛擬機遇到一個須要使用到變量的值,就會使用到這個指令
做用於工做內存中的變量,它把一個從執行引擎中接受到的值放入工做內存的變量副本中
做用於主內存中的變量,它把一個從工做內存中一個變量的值傳送到主內存中,以便後續的write使用
做用於主內存中的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中
單看這八種類型的原子操做可能有點抽象,咱們畫一個操做流程圖仔細梳理下。
操做流程圖:
從圖中能夠看出,若是要把一個變量從內存中複製到工做內存中,就須要順序的執行read和load操做,若是把變量從工做內存同步到主內存中,就須要執行store和write操做。
NOTE: Java內存模型只要求上述操做必須按順序執行,卻沒要求是連續執行。
咱們以兩個線程爲例梳理下操做流程:
假設存在兩個線程A和B,若是線程A要與線程B要通訊的話,首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去;而後,線程B到主內存中讀取線程A以前已經更新過的共享變量。
敏銳的小夥伴可能會發現,若是多個線程同時讀取修改同一個共享變量,這種狀況可能會致使每一個線程中的本地內存中緩存變量一致的問題,這個時候該怎麼解決呢?
解決JMM中的本地內存變量的緩存不一致問題有兩種解決方案,分別是總線加鎖
和MESI緩存一致性協議
。
總線加鎖
總線加鎖是CPU從主內存讀取數據到本地內存時,會先在總線對這個數據加鎖,這樣其它CPU就無法去讀或者去寫這個數據,直到這個CPU使用完數據釋放鎖後,,其它的CPU才能讀取該數據。
總線加鎖雖然能保證數據一致,可是它卻嚴重下降了系統性能,由於當一個線程多總線加鎖後,其它線程都只能等待,將原有的並行操做轉成了串行操做。
一般狀況下,咱們不採用這種方法,而是使用性能較高的緩存一致性協議。
MESI緩存一致性協議
MESI緩存一致性協議是多個CPU從主內存讀取同一個數據到各自的高速緩存中,當其中的某個CPU修改了緩存裏的數據,該數據會立刻同步回主內存,其它CPU經過總線嗅探機制能夠感知到數據的變化從而將本身緩存裏的數據失效。
在併發編程中,若是多個線程對同一個共享變量進行操做是,咱們一般會在變量名稱前加上關鍵在volatile
,由於它能夠保證線程對變量的修改的可見性,保證可見性的基礎是多個線程都會監聽總線。即當一個線程修改了共享變量後,該變量會立馬同步到主內存,其他線程監聽到數據變化後會使得本身緩存的原數據失效,並觸發read
操做讀取新修改的變量的值。進而保證了多個線程的數據一致性。事實上,volatile
的工做原理就是依賴於MESI緩存一致性協議實現的。
在Java多線程中,Java提供了一系列與併發處理相關的關鍵字,好比volatile
、synchronized
、final
、concurren
包等。其實這些就是Java內存模型封裝了底層的實現後提供給程序員使用的一些關鍵字
事實上,Java內存模型的本質是圍繞着Java併發過程當中的如何處理原子性
、可見性
和順序性
這三個特徵來設計的,這三大特性能夠直接使用Java中提供的關鍵字實現,它們也是面試中常常被問到的題目。
原子性的定義是一個操做不能被打斷,要麼所有執行完畢,要麼不執行。在這點上有點相似於事務操做,要麼所有執行成功,要麼回退到執行該操做以前的狀態。
JMM保證的原子性變量操做包括read、load、assign、use、store、write
NOTE:基本類型數據的訪問大都是原子操做,long 和double類型的變量是64位,可是在32位JVM中,32位的JVM會將64位數據的讀寫操做分爲2次32位的讀寫操做來進行,這就致使了long、double類型的變量在32位虛擬機中是非原子操做,數據有可能會被破壞,也就意味着多個線程在併發訪問的時候是線程非安全的。
對於非原子操做的基本類型,可使用synchronized來保證方法和代碼塊內的操做是原子性的。
1 synchronized (this) {
2 a=1;
3 b=2;
4 }
複製代碼
如一個線程觀察另一個線程執行上面的代碼,只能看到a、b都被賦值成功結果,或者a、b都還沒有被賦值的結果。
Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存做爲傳遞媒介的方式來實現的。
Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,可使用volatile來保證多線程操做時變量的可見性。
除了volatile,Java中的synchronized和final兩個關鍵字也能夠實現可見性。只不過實現方式不一樣,這裏再也不展開了。
在Java中,可使用synchronized和volatile來保證多線程之間操做的有序性。實現方式有所區別:
volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只容許一條線程操做。
好了,這裏簡單的介紹完了Java併發編程中解決原子性、可見性以及有序性可使用的關鍵字。讀者可能發現了,好像synchronized關鍵字是萬 能的,他能夠同時知足以上三種特性,這其實也是不少人濫用synchronized的緣由。
可是synchronized是比較影響性能的,雖然編譯器提供了不少鎖優化技術,可是也不建議過分使用。
參考文獻
[1]https://www.jianshu.com/p/8a58d8335270 [2]https://blog.csdn.net/javazejian/article/details/72772461 [3]https://blog.csdn.net/zjcjava/article/details/78406330 [4]https://segmentfault.com/a/1190000016085105