哈嘍,大家好🎉,我是世傑。
本文我為大家介紹面試官經常考察的「Java記憶體模型JMM相關內容」
面試連環call
- 什麼是Java記憶體模型(JMM)? 為什麼需要JMM?
- Java執行緒的工作記憶體和主記憶體各自的作用?
- Java快取一致性問題?
- Java的併發程式設計問題?
要想理解透徹 JMM(Java 記憶體模型),我們先要從 『硬體記憶體結構』 說起。讓我們開始吧!🎉🎉🎉
1. 硬體記憶體結構
1.1 CPU 快取模型
(1)CPU Register
CPU Register 也就是 CPU 暫存器。CPU 暫存器是 CPU 內部整合的,在暫存器上執行操作的效率要比在主存上高出幾個數量級。
(2)CPU Cache Memory
CPU Cache Memory 也就是 CPU 快取記憶體,相對於暫存器來說,通常也可以成為 L2 二級快取。相對於硬碟讀取速度來說記憶體讀取的效率非常高,但是與 CPU 還是相差數量級,所以在 CPU 和主存間引入了多級快取。CPU 快取則是為了解決 CPU 處理速度和記憶體處理速度不對等的問題。
(3)Main Memory
Main Memory 就是主存,主存比 L1、L2 快取要大很多。
1.2 快取一致性問題
由於主存與 CPU 處理器的運算能力之間有數量級的差距,所以在傳統計算機記憶體架構中會引入快取記憶體來作為主存和處理器之間的緩衝。先複製一份資料到 CPU Cache 中,當 CPU 需要用到的時候就可以直接從 CPU Cache 中讀取資料,當運算完成後,再將運算得到的資料寫回 Main Memory 中。
但是,這樣存在 記憶體快取不一致性的問題 !比如我執行一個 i++ 操作的話,如果兩個執行緒同時執行的話,假設兩個執行緒從 CPU Cache 中讀取的 i=1,兩個執行緒做了 i++ 運算完之後再寫回 Main Memory 之後 i=2,而正確結果應該是 i=3。
CPU 為了解決記憶體快取不一致性問題可以透過制定快取一致協議(比如 [MESI 協議open in new window])或者其他手段來解決。 這個快取一致性協議指的是在 CPU 快取記憶體與主記憶體互動的時候需要遵守的原則和規範。不同的 CPU 中,使用的快取一致性協議通常也會有所不同。
1.3 指令重排序
什麼是指令重排序? 簡單來說就是系統在執行程式碼的時候並不一定是按照你寫的程式碼的順序依次執行。
常見的指令重排序有下面 2 種情況:
- 編譯器最佳化重排:編譯器(包括 JVM、JIT 編譯器等)在不改變單執行緒程式語義的前提下,重新安排語句的執行順序。
- 指令並行重排:現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
- 記憶體系統的重排序:由於處理器使用快取和讀寫緩衝區,這使得載入和儲存操作看上去可能是在亂序執行。
Java 原始碼會經歷 編譯器最佳化重排 —> 指令並行重排 —> 記憶體系統重排 的過程,最終才變成作業系統可執行的指令序列。
2. Java記憶體模型
2.1 Java記憶體與硬體記憶體
之前講 JVM 執行時記憶體區域時,聊到 JVM 分為棧、堆等,這些都是 JVM 定義的概念。在傳統的硬體記憶體架構中是沒有棧和堆這種概念。從圖中可以看出棧和堆既存在於快取記憶體中又存在於主記憶體中,所以Java記憶體和硬體記憶體沒有直接的關係。
2.2 Java記憶體與主記憶體
(1) Java 記憶體模型規定所有的變數都儲存在主記憶體中,每條執行緒都有自己的工作記憶體。
(2) 執行緒的工作記憶體中儲存了被該執行緒使用到的變數的主記憶體副本複製,執行緒對變數的所有操作(讀取、賦值等)都必須在工作記憶體中進行,而不能直接讀寫主記憶體中的變數。
(3) 不同執行緒之間也無法直接訪問對方工作記憶體中的變數,執行緒間變數值的傳遞都需要透過主記憶體來完成。
-
主記憶體:所有執行緒建立的例項物件都存放在主記憶體中,不管該例項物件是成員變數,還是區域性變數,類資訊、常量、靜態變數都是放在主記憶體中。為了獲取更好的執行速度,虛擬機器及硬體系統可能會讓工作記憶體優先儲存於暫存器和快取記憶體中。
-
本地記憶體:每個執行緒都有一個私有的本地記憶體,本地記憶體儲存了該執行緒以讀 / 寫共享變數的副本。每個執行緒只能操作自己本地記憶體中的變數,無法直接訪問其他執行緒的本地記憶體。如果執行緒間需要通訊,必須透過主記憶體來進行。本地記憶體是 JMM 抽象出來的一個概念,並不真實存在,它涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器最佳化。
為了更好的控制主記憶體和本地記憶體的互動,Java 記憶體模型定義了八種操作來實現:
『主記憶體』
-
lock:鎖定。作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態
-
unlock:解鎖。作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定
-
read:讀取。作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
-
write:寫入。作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中
『工作記憶體』
-
load:載入。作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中
-
use:使用。作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作
-
assign:賦值。作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作
-
store:儲存。作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作
3. 再聊併發程式設計
熟悉 Java 併發程式設計的同學肯定對這三個問題很熟悉:『可見性問題』、『原子性問題』、『有序性問題』。
3.1 有序性
由於指令重排序問題,程式碼的執行順序未必就是編寫程式碼時候的順序。我們上面講重排序的時候也提到過:指令重排序可以保證序列語義一致,但是沒有義務保證多執行緒間的語義也一致 ,所以在多執行緒下,指令重排序可能會導致一些問題。
在 Java 中,volatile
關鍵字可以禁止指令進行重排序最佳化。
3.2 原子性
一次操作或者多次操作,要麼所有的操作全部都得到執行並且不會受到任何因素的干擾而中斷,要麼都不執行。
在 Java 中,可以藉助synchronized
、各種 Lock
以及各種原子類實現原子性。
3.3 可見性
當一個執行緒對共享變數進行了修改,那麼另外的執行緒都是立即可以看到修改後的最新值。
在 Java 中,可以藉助synchronized
、volatile
以及各種 Lock
實現可見性。
參考文章
-
Java 記憶體模型引入
-
JMM(Java 記憶體模型)詳解
-
說說什麼是Java記憶體模型?
-
從 CPU 講起,深入理解 Java 記憶體模型!