Java內存模型雖然說是一個老生常談的問題 ,也是大廠面試中繞不過的,甚至初級面試也會問到。可是真正要理解起來,仍是至關困難,主要這個東西看不見,摸不着。網上已經有大量的博客,可是人家的終究是人家的,本身也要好好的去理解,去消化。今天我也來班門弄斧,說下Java內存模型。程序員
說到Java內存模型,不得不說到 計算機硬件方面的知識。面試
咱們都知道CPU 和 內存是計算機中比較核心的兩個東西,它們之間會頻繁的交互,隨着CPU發展愈來愈快,內存的讀寫的速度遠遠不如CPU的處理速度,因此CPU廠商在CPU上加了一個 高速緩存,用來緩解這種問題。咱們在看CPU硬件參數的時候,也會看到有這樣的參數:編程
通常高速緩存有3級:L1,L2,L3,CPU與內存的交互,就發生了變化,CPU再也不與內存直接交互,CPU會先去L1中尋找數據,沒有的話,再去L2中尋找,而後是L3,最後纔去內存尋找(更準確的來講,應該是CPU中的寄存器去尋找)。緩存
咱們能夠畫一張圖來理解:多線程
看起來一切都很美好,可是隨着科技的進步,CPU廠商們叒搞事了,推出了多核CPU,每一個CPU上又有高速緩存,CPU與內存的交互就變成了下面這個樣子:架構
這樣就會引起一個問題:緩存不一致。
爲何會出現這個問題呢?
CPU須要修改某個數據,是先去Cache中找,若是Cache中沒有找到,會去內存中找,而後把數據複製到Cache中,下次就不須要再去內存中尋找了,而後進行修改操做。而修改操做的過程是這樣的:在Cache裏面修改數據,而後再把數據刷新到主內存。其餘CPU須要讀取數據,也是先去Cache中去尋找,若是找到了就不會去內存找了。
因此當兩個CPU的Cache同時都擁有某個數據,其中一個CPU修改了數據,另一個CPU是無感知的,並不知道這個數據已經不是最新的了,它要讀取數據仍是從本身的Cache中讀取,這樣就致使了「緩存不一致」。
其實對於這樣的描述並非十分準確,由於計算、讀取等操做都是在CPU的寄存器中進行的,這樣的描述是爲了讓問題變得更簡單,相信學過計算機體系的人應該很是清楚整個流程,在這裏就簡單的描述下。
解決這個問題的方法有不少,好比:
總線加鎖(此方法性能較低,如今已經不會再使用)MESI協議
這是Intel提出的,MESI協議也是至關複雜,在這裏我就簡單的說下:當一個CPU修改了Cache中的數據,會通知其餘緩存了這個數據的CPU,其餘CPU會把Cache中這份數據的Cache Line置爲無效,要讀取數據的話,直接去內存中獲取,不會再從Cache中獲取了。
固然還有其餘的解決方案,MESI協議是其中比較出名的。
Java線程與硬件處理器
其實,咱們在Java中開啓一個線程,最終Java也會交給CPU去執行。
具體的流程是:咱們在使用Java線程,內部會調用操做系統(OS)的內核線程(Kernel-Level Thread),這種線程是操做系統內核(Kernel)直接支持的,內核經過調度器,對線程進行調度,並將線程交給各個CPU內核去處理。
以下圖所示:併發
看到標題,你們確定會想:我靠,難道上面說的都和Java內存模型沒有關係嗎,從這裏纔是真正介紹Java內存模型嗎?其實,並非,Java內存模型是一個抽象的概念,其實並不存在,它描述的是一種規範,最終Java程序都會交給CPU去運行,因此上面是計算機硬件體系是基礎,有了上面的基礎,纔有了Java內存模型,或者說Java的內存模型就是利用了計算機硬件體系。app
仍是從一張圖來入手:性能
本地內存:存放的是 私有變量 和 主內存數據的副本。若是私有變量是基本數據類型,則直接存放在本地內存,若是是引用類型變量,存放的是引用(指針),實際的數據存放在主內存。本地內存是不共享的,只有屬於它的線程能夠訪問。也有好多人把 本地內存 稱之爲 線程棧 或者 工做空間。spa
主內存:存放的是共享的數據,全部線程均可以訪問。固然它也有很多其餘稱呼,好比 堆內存,共享內存等等。
Java內存模型規定了全部對共享變量的讀寫操做都必須在本地內存中進行,須要先從主內存中拿到數據,複製到本地內存,而後在本地內存中對數據進行修改,再刷新回主內存。
經過前面的鋪墊,咱們應該認識到Java的執行最終仍是會交給CPU去處理,可是Java的內存模型和硬件架構又不徹底一致。對於硬件來講,只有CPU,Cache和主內存,並無Java內存模型中本地內存(線程棧、工做空間)或者主內存(共享內存,堆內存)的概念,因此不論是Java內存模型中的本地內存,仍是主內存的數據,最終都會存儲在CPU(更準確的來講 是寄存器)、Cache、內存上。
因此,Java內存模型和計算機硬件架構存在這樣的關係:
Java內存模型就是爲了解決多線程對共享數據的讀寫一致性問題。
併發編程中三個重要特性
原子性:
不可分割,同生共死。
i=1
具備原子性,直接 在本地內存中進行賦值操做。
i++;
不具備原子性,有三個步驟
1.把i讀取出來(原子性)
2.在本地內存中作自增運算(原子性)
3.再把值寫回i(原子性)
多個原子性操做組合在一塊兒,就不具備原子性了。
通常狀況下,基本數據類型的賦值,讀取都是具備原子性的。
可見性
一個線程在本地內存中修改了共享內存的數據,對於其餘持有該數據的線程是「不可見」的。
有序性
代碼在運行的時候,執行順序可能並非嚴格從上到下執行的,會進行指令重排。根據CPU流水線做業,通常來講 簡單的操做會先執行,複雜的操做後執行。
指令重排會有兩個規則:
as-if-seria
無論怎麼重排序,單線程的執行結果不能發生改變。正是因爲這個特性,在單線程中,程序員通常無需理會重排序帶來的問題。
happens-before
程序次序規則
一段代碼在單線程中執行的結果是有序的(感受就是as-if-seria原則)volatile規則(之後會花一整節內容介紹,這裏不展開)鎖定規則若是鎖處於Lock的狀態,必須等Unlock後,才能再次進行Lock操做。
傳遞規則
A happens-before B , B happens-before C,那麼A happens-before C。
Java內存模型是個至關複雜的東西,在這裏只能說是「走馬觀花 」般的介紹下。但願經過本文介紹,你們能夠對Java模型有一個初步的瞭解。