Java記憶體模型雖說是一個老生常談的問題 ,也是大廠面試中繞不過的,甚至初級面試也會問到。但是真正要理解起來,還是相當困難,主要這個東西看不見,摸不著。網上已經有大量的博客,但是人家的終究是人家的,自己也要好好的去理解,去消化。今天我也來班門弄斧,說下Java記憶體模型。 說到Java記憶體模型,不得不 ...
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記憶體模型是一個抽象的概念,其實並不存在,它描述的是一種規範,最終Java程式都會交給CPU去運行,所以上面是電腦硬體體系是基礎,有了上面的基礎,才有了Java記憶體模型,或者說Java的記憶體模型就是利用了電腦硬體體系。
還是從一張圖來入手:
本地記憶體:存放的是 私有變數 和 主記憶體數據的副本。如果私有變數是基本數據類型,則直接存放在本地記憶體,如果是引用類型變數,存放的是引用(指針),實際的數據存放在主記憶體。本地記憶體是不共用的,只有屬於它的線程可以訪問。也有好多人把 本地記憶體 稱之為 線程棧 或者 工作空間。
主記憶體:存放的是共用的數據,所有線程都可以訪問。當然它也有不少其他稱呼,比如 堆記憶體,共用記憶體等等。
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
- 程式次序規則
一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作。 - volatile規則(以後會花一整節內容介紹,這裡不展開)
- 鎖定規則
如果鎖處於Lock的狀態,必須等Unlock後,才能再次進行Lock操作。 - 傳遞規則
A happens-before B , B happens-before C,那麼A happens-before C。
- 程式次序規則
Java記憶體模型是個相當複雜的東西,我在這裡可能還說不上是談,只能說是“蜻蜓點水 ”般的介紹下。希望通過這篇文章,大家可以對Java模型有一個初步的瞭解。
以後,我也會介紹Synchronized 和 volatile關鍵字等等,我可能會再次提到本節中涵蓋的內容,並做進一步的補充說明。
好了,本文的內容到這裡就結束了,在寫之前,已經做好心理準備了,可能需要花上半天時間,但是實際上遠遠不止半天,在寫的過程中,翻閱了大量的文章,包括 知乎、博客園、簡書 等等,發現 如果要“較真”“抬杠”的話,文章與文章之間也有有衝突的地方,甚至一篇文章中,也有前後矛盾的地方。我也不奢求本文中介紹的所有內容都是正確的。為了不誤人子弟,如果大家發現有錯誤,希望可以及時向我提出,我也會儘快核實後修改。
感謝大家可以看到最後,再見。