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(原子性)
多個原子性操作組合在一起,就不具有原子性了。
一般情況下,在64位操作系統之下,基本數據類型的賦值,讀取都是具有原子性的。
可見性
一個線程在本地內存中修改了共享內存的數據,對於其他持有該數據的線程是“不可見”的。
有序性
代碼在運行的時候,執行順序可能並不是嚴格從上到下執行的,會進行指令重排。
根據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關鍵字等等,我可能會再次提到本節中涵蓋的內容,並做進一步的補充說明。
好了,本文的內容到這里就結束了,在寫之前,已經做好心理准備了,可能需要花上半天時間,但是實際上遠遠不止半天,在寫的過程中,翻閱了大量的文章,包括 知乎、博客園、簡書 等等,發現 如果要“較真”“抬杠”的話,文章與文章之間也有有沖突的地方,甚至一篇文章中,也有前后矛盾的地方。我也不奢求本文中介紹的所有內容都是正確的。為了不誤人子弟,如果大家發現有錯誤,希望可以及時向我提出,我也會盡快核實后修改。
感謝大家可以看到最后,再見。