死磕並發:Java記憶體模型

  • 2019 年 10 月 4 日
  • 筆記

前言

首先我們在了解java記憶體模型之前先看一下電腦記憶體模型,理解了電腦記憶體模型的話後面在看JMM就會簡單的多,上篇文章我是直接寫的。

電腦記憶體

電腦是由CPU、主存、磁碟等組成的(簡單引出問題熬)我們都知道電腦執行程式的指令都是由CPU來執行的,執行的時候是要處理數據的,這些數據通常存儲在主存中。

如圖所示,這時候問題來了,CPU的執行速度越來越快,然後記憶體倒是沒什麼進展,這樣的話CPU的讀寫操作就會非常耗時,效率不就很低了?

所以這個時候就出現了高速快取(Cache)來解決這個問題,那麼快取是什麼呢?快取其實就是保存的數據備存,特點是快。所以這個時候程式的執行過程就變成了這個樣子:首先在運行的時候會把數據從主存中賦值一份放在快取中,然後CPU在運算的時候就直接去快取中讀寫數據,等執行結束後在把數據刷新到主存中。這樣一來就大大的提高了執行的速度。我們來看一下流程圖:

可以看出,運行的時候L1快取先把數據從主存中讀取出來,然後CPU操作的數據是從快取中讀取,當數據執行完畢,在從快取中刷新到主存中。隨著CPU的執行能力越來越強,一層快取已經滿足不了需求了,這時候就出現了2級快取(L2Cache)3級快取(L3Cache),每級快取都存儲的是下一級快取的一部分數據。

那麼當CPU需要數據的時候就會這樣執行:首先去一級快取(L1Cache)查找,如果一級快取沒有就去二級快取(L2Cache)查找,二級快取沒有就去三級快取(L3Cache)查找,如果快取中沒有,就去主存中查找。 那麼問題來了。

快取一致性

現代電腦已經不是單個CPU,有多個CPU每個CPU還可能會有多核,單核CPU只有一套快取分別就是上面所說的L1、L2、L3如圖所示:

如果CPU有多個核心的話,就是每個核心都有L1快取或者有L2快取,而共享L3快取或者L2快取。

我們來看一下結構圖:

這個時候每個核心都有自己的高速快取,它們又共享同一主存,就會造成快取一致性的問題,在多執行緒同時訪問同一共享數據的情況下,每個執行緒都是操作自己快取的數據副本,這個時候就會出現每個快取中的共享數據存在不一致的情況。多個處理器運算任務都涉及同一塊主存,需要一種協議可以保障數據的一致性,這類協議有MSI、MESI、MOSI及Dragon Protocol等。

處理器優化

上面了解到提高CPU的效率就是在CPU和主存直接增加高速快取,增加高速快取會造成快取不一致的問題,除了快取不一致的問題,還有一種問題就是為了能讓處理器內部的運算單元能夠盡量的被充分利用處理器可能會對輸入程式碼進行亂序執行,並且處理器會在計算之後將亂序的程式碼進行結果重組來保證結果的一致性。在Java虛擬機中也有類似的指令重排序

思考

這篇文章其實是講述java記憶體模型的,為什麼會和電腦硬體扯上關係呢?注意到上面有說到多執行緒的情況下會造成快取不一致的問題,提到多執行緒就離不開並發,想到並發的話就離不開三大問題,可見性,原子性,有序性的問題。那這三種特性不就是上面所說到的快取不一致,處理器優化和指令重排序問題嗎。這這樣看來快取不一致不就是可見性的問題,而原子性不就是處理器優化所導致的原子性問題,指令重排序就是導致有序性的問題。那麼Java記憶體模型又是什麼呢?

java記憶體模型

Java記憶體模型的作用就是用來屏蔽掉不同作業系統中的記憶體差異性來保持並發的一致性。同時JMM也規範了JVM如何與電腦記憶體進行交互。簡單的來說java記憶體模型就是Java自己的一套協議來屏蔽掉各種硬體和作業系統的記憶體訪問差異,實現平台一致性達到最終的"一次編寫,到處運行"。看到這裡就知道了Jmm是用來做什麼的。同時Java記憶體模型可以理解為java並發記憶體模型。然後JMM

通訊

Java記憶體模型(以下簡稱JMM)規定了,所有變數都存儲在主記憶體中,每個執行緒都有自己的本地快取,所以執行緒中對變數的操作都必須在本地快取中進行並不是直接操作主記憶體,執行緒之間的無法訪問對方執行緒的變數,想要通訊的話就只能通過主記憶體進行通訊。

JMM抽象示意圖:

從上圖可以看出每個執行緒都有一個本地記憶體,如果執行緒想要通訊的話要執行一下步驟:

  • A執行緒先把本地記憶體的值寫入主記憶體
  • B執行緒從主記憶體中去讀取出A執行緒寫的值

具體通訊規則可以參考我上一篇文章:Java記憶體模型裡面定義了八種通訊規則。

到這裡就對JMM有個清晰的理解了。JMM其實是一種規範,其主要目的就是為了解決多執行緒通過共享記憶體進行通訊時所產生的本地記憶體數據不一致,編譯器會對程式碼指令重排序、處理器會對程式碼亂序執行等帶來的問題。

解決的問題

JMM所解決的問題離不開我們上面所說的三大特性:可見性、原子性、有序性.

原子性:在java中使用synchronized關鍵字保證程式碼的原子性,synchronized實現原理後面會單獨寫一篇文章。

可見性:volatile關鍵字保證了多執行緒操控變數的可見性,同時synchronized和final也可以保證變數的可見性,注意:volatile並不保證原子性,所以什麼時候用volatile一定要注意。

有序性:volatile可以禁用指令重排,synchronized關鍵字保證同一時刻只允許一條執行緒操作所以我們可以發現synchronized可以解決三種問題,所以使用synchronized關鍵字比較多,但是synchronized只允許一個執行緒進行操作,會造成上下文切換的效率問題。

總結

通過上文一定對JMM是什麼,和有什麼作用有了一定的理解