JMM(java memory model)java內存模型主要目標是定義程序中的變量,(此處所指的變量是實例字段、靜態字段等,不包含局部變量和函數參數,因為這兩種是線程私有無法共享)在虛擬機中存儲到內存與從內存讀取出來的規則細節,Java 內存模型規定所有變量都存儲在主內存中,每條線程還有自己的工作內存,工作內存保存了該線程使用到的變量到主內存副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在自己的工作內存中進行而不能直接讀寫主內存的變量,不同線程之間無法相互直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要在主內存來完成。
Java 內存模型對主內存與工作內存之間的具體交互協議定義了八種操作,具體如下:
-
lock(鎖定):作用於主內存變量,把一個變量標識為一條線程獨占狀態。
-
unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
-
read(讀取):作用於主內存變量,把一個變量從主內存傳輸到線程的工作內存中,以便隨后的 load 動作使用。
-
load(載入):作用於工作內存變量,把 read 操作從主內存中得到的變量值放入工作內存的變量副本中。
-
use(使用):作用於工作內存變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量值的字節碼指令時執行此操作。
-
assign(賦值):作用於工作內存變量,把一個從執行引擎接收的值賦值給工作內存的變量,每當虛擬機遇到一個需要給變量進行賦值的字節碼指令時執行此操作。
-
store(存儲):作用於工作內存變量,把工作內存中一個變量的值傳遞到主內存中,以便后續 write 操作。
-
write(寫入):作用於主內存變量,把 store 操作從工作內存中得到的值放入主內存變量中。
如果要把一個變量從主內存復制到工作內存就必須按順序執行 read 和 load 操作,從工作內存同步回主內存就必須順序執行 store 和 write 操作,但是 JVM 只要求了操作的順序而沒有要求上述操作必須保證連續性,所以實質執行中 read 和 load 間及 store 和 write 間是可以插入其他指令的。
Java 內存模型還會對指令進行重排序操作,在執行程序時為了提高性能編譯器和處理器經常會對指令進行重排序操作,重排序主要分下面幾類:
-
編譯器優化重排序:編譯器在不改變單線程程序語義的前提下可以重新安排語句的執行順序。
-
指令級並行重排序:現代處理器采用了指令級並行技術來將多條指令重疊執行,如果不存在數據依賴性處理器可以改變語句對應機器指令的執行順序。
-
內存系統重排序:由於處理器使用緩存和讀寫緩沖區使得加載和存儲操作看上去可能是在亂序執行。
其實 Java JMM 內存模型是圍繞並發編程中原子性、可見性、有序性三個特征來建立的,關於原子性、可見性、有序性的理解如下:
-
原子性:就是說一個操作不能被打斷,要么執行完要么不執行,類似事務操作,Java 基本類型數據的訪問大都是原子操作,long 和 double 類型是 64 位,在 32 位 JVM 中會將 64 位數據的讀寫操作分成兩次 32 位來處理,所以 long 和 double 在 32 位 JVM 中是非原子操作,也就是說在並發訪問時是線程非安全的,要想保證原子性就得對訪問該數據的地方進行同步操作,譬如 synchronized 等。
-
可見性:就是說當一個線程對共享變量做了修改后其他線程可以立即感知到該共享變量的改變,從 Java 內存模型我們就能看出來多線程訪問共享變量都要經過線程工作內存到主存的復制和主存到線程工作內存的復制操作,所以普通共享變量就無法保證可見性了;Java 提供了 volatile 修飾符來保證變量的可見性,每次使用 volatile 變量都會主動從主存中刷新,除此之外 synchronized、Lock、final 都可以保證變量的可見性。
-
有序性:就是說 Java 內存模型中的指令重排不會影響單線程的執行順序,但是會影響多線程並發執行的正確性,所以在並發中我們必須要想辦法保證並發代碼的有序性;在 Java 里可以通過 volatile 關鍵字保證一定的有序性,還可以通過 synchronized、Lock 來保證有序性,因為 synchronized、Lock 保證了每一時刻只有一個線程執行同步代碼相當於單線程執行,所以自然不會有有序性的問題;除此之外 Java 內存模型通過 happens-before 原則如果能推導出來兩個操作的執行順序就能先天保證有序性,否則無法保證,關於 happens-before 原則可以查閱相關資料。
所以說如果想讓 Java 並發程序正確的執行必須保證原子性、有序性、可見性,只要三者中有任意一個不滿足並發都無法正確執行。