什麼是Java內存模型(JMM)?
一、JMM的相關概念
Java內存模型簡稱JMM(Java Memory Model),是Java虛擬機所定義的一種抽象規範,用來屏蔽不同硬件和操作系統的內存訪問差異,讓java程序在各種平臺下都能達到一致的內存訪問效果。
這裏要注意兩點:
1)JMM是一個抽象的概念,並不是物理上的內存劃分。
2)JMM-JVM-硬件的關係:Java內存模型(JMM)定義了Java虛擬機(JVM)在計算機內存(RAM)中的工作規範。在硬件內存模型中,各種CPU架構的實現是不盡相同的,Java作爲跨平臺的語言,爲了屏蔽底層硬件差異,定義了Java內存模型(JMM)。JMM作用於JVM和底層硬件之間,屏蔽了下游不同硬件模型帶來的差異,爲上游開發者提供了統一的使用接口。
總之一句話:JMM是JVM的內存使用規範,是一個抽象的概念。
Java內存模型的抽象示意圖:
如上圖在JMM中,內存劃分爲兩個區域,主內存和工作內存。
1. 主內存(Main Memory)
主內存被所有的線程所共享,所有的變量都存儲在主內存中,包括實例變量,靜態變量,但是不包括局部變量和方法參數。
2. 工作內存(Working Memory)
每一個線程擁有自己的工作內存,線程的工作內存保存了該線程用到的變量和主內存的副本拷貝。
說明:
1) 每個線程的工作內存都是獨立的,線程操作數據只能在工作內存中進行,然後刷回到主內存。這是 Java 內存模型定義的線程基本工作方式。
2)線程對共享變量的所有操作都必須在工作內存進行,不能直接讀寫主內存中的變量。不同線程之間也無法訪問彼此的工作內存,變量值的傳遞只能通過主內存來進行。
二、JMM定義了什麼?
整個Java內存模型實際上是圍繞着三個特徵建立起來的。分別是:原子性,可見性,有序性。這三個特徵可謂是整個Java併發的基礎。
1. 原子性
原子性指的是一個操作是不可分割,不可中斷的,一個線程在執行時不會被其他線程干擾。
下面的這幾句代碼能保證原子性嗎?我們一起來看下
1 int i = 2; 2 3 int j = i; 4 5 i++; 6 7 i = i + 1;
第一句是基本類型賦值操作,必定是原子性操作。
第二句先讀取i的值,再賦值到j,兩步操作,不能保證原子性。
第三和第四句其實是等效的,先讀取i的值,再+1,最後賦值到i,三步操作了,不能保證原子性。
實現方式:在 Java 中, JMM只能保證基本的原子性。如果要保證一個代碼塊的原子性,可以藉助synchronized(提供了monitorenter 和 moniterexit 兩個字節碼指令)、各種 Lock 以及各種原子類實現原子性。synchronized 和各種 Lock 可以保證任一時刻只有一個線程訪問該代碼塊,因此可以保障原子性。各種原子類是利用 CAS (compare and swap)
2. 可見性
可見性指當一個線程修改共享變量的值,其他線程能夠立即知道被修改了。
實現方式:在 Java 中,可以藉助synchronized、volatile 以及各種 Lock 實現可見性。
如果我們將變量聲明爲 volatile ,這就指示 JVM,這個變量是共享且不穩定的,每次使用它都到主內存中進行讀取。
synchronized的原理是,在執行完,進入unlock之前,必須將共享變量同步到主內存中。
final修飾的字段,一旦初始化完成,如果沒有對象逸出(指對象未初始化完成就可以被別的線程使用),那麼對於其他線程都是可見的。
3. 有序性
有序性即程序執行的順序按照代碼的先後順序執行。
說明:由於指令重排序問題,代碼的執行順序未必就是編寫代碼時候的順序。指令重排序可以保證串行語義一致,但是沒有義務保證多線程間的語義也一致 ,所以在多線程下,指令重排序可能會導致一些問題。
實現方式:在 Java 中,可以使用synchronized或者volatile保證多線程之間操作的有序性。
volatile關鍵字是使用內存屏障達到禁止指令重排序,以保證有序性。
synchronized的原理是,一個線程lock之後,必須unlock後,其他線程纔可以重新lock,使得被synchronized包住的代碼塊在多線程之間是串行執行的。
三、八種內存交互操作
內存交互操作有8種,如下圖:
1. lock(鎖定)
作用於主內存的變量,把一個變量標識爲線程獨佔的狀態。
2. read(讀取)
作用於主內存的變量,把一個變量值從主內存傳輸到線程的工作內存中,以便下一步的load操作使用。
3. load(載入)
作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
4. use(使用)
作用於工作內存中的變量,表示把工作內存中的一個變量的值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時就會執行該操作。
5. assign(賦值)
作用於工作內存的變量,它把一個從執行引擎返回的結果賦值給工作內存的變量副本中,每當虛擬機遇到一個給變量賦值的字節碼指令時將會執行這個操作。
6. store(存儲)
作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便後續的write的操作。
7. wirte(寫入)
作用於主內存的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中。
8. unlock(解鎖)
作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量纔可以被其他線程鎖定。
說明:
1)如果要把一個變量從主內存中複製到工作內存中,就需要按順序地執行 read 和 load 操作。
2)如果把變量從工作內存中同步到主內存中,就需要按順序地執行 store 和 write 操作。
但Java 內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。
下面繼續補充一下JMM對8種內存交互操作制定的規則:
1)不允許read、load、store、write操作之一單獨出現,也就是read操作後必須load,store操作後必須write。
2)不允許線程丟棄他最近的assign操作,即工作內存中的變量數據改變了之後,必須告知主存。
3)不允許線程將沒有assign的數據從工作內存同步到主內存。
4)一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。就是對變量實施use、store操作之前,必須經過load和assign操作。
5)一個變量同一時間只能有一個線程對其進行lock操作。多次lock之後,必須執行相同次數unlock纔可以解鎖。
6)如果對一個變量進行lock操作,會清空所有工作內存中此變量的值。在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值。
7)如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。
8)一個線程對一個變量進行unlock操作之前,必須先把此變量同步回主內存。
四、Java 內存結構和 JMM 有何區別?
這是一個比較常見的問題,很多初學者非常容易搞混。
Java 內存結構和內存模型是完全不一樣的兩個東西。
1. Java內存結構
Java 內存結構和 Java 虛擬機的運行時區域相關,定義了 JVM 在運行時如何分區存儲程序數據,就比如說堆主要用於存放對象實例。
2. Java內存模型(JMM)
Java 內存模型和 Java 的併發編程相關,抽象了線程和主內存之間的關係就比如說線程之間的共享變量必須存儲在主內存中,規定了從 Java 源代碼到 CPU 可執行指令的這個轉化過程要遵守哪些和併發相關的原則和規範,其主要目的是爲了簡化多線程編程,增強程序可移植性的。
參考鏈接:
https://zhuanlan.zhihu.com/p/258393139