JMM理解
JMM概念
JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念JMM(Java內存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念并不真實存在,它描述的一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段、靜態字段何構成數組對象的元素)的訪問方式。
并不真實存在,它描述的一組規則或規范,通過這組規范定義了程序中各個變量(包括實例字段、靜態字段何構成數組對象的元素)的訪問方式。
JMM關于同步的規定:
1、線程解鎖前,必須把共享變量的值刷新回主內存
2、線程加鎖前,必須讀取主內存的最新值到自己的工作內存
3、加鎖解鎖是同一把鎖
由于JVM運行程序的實體是線程,而每個線程創建時JVM都會為其創建一個工作內存(有些地方稱為棧空間),工作內存是每個線程的私有數據區域,而Java內存模型中的規定所有變量都存儲在主內存,主內存是共享內存區域,所有線程都可以訪問,但線程對變量的操作(讀取賦值等)必須在工作內存中進行,首先要將變量從主內存拷貝到線程自己的工作內存空間,然后對變量進行操作,操作完成后再將變量寫會主內存,不能直接操作主內存中的變量,各個線程中的工作內存中存儲著主內存中的變量副本拷貝,因此不同的線程間無法訪問對方的工作內存,線程間的通信(傳值)必須通過主內存來完成,其簡要訪問過程入下圖:
JMM 的八種內存交互操作
為了更直觀,先來看看這張圖吧:
- lock(鎖定):作用于主內存中的變量,一個變量在同一時間只能被一個線程鎖定,即把變量標識為線程獨占狀態。
- read(讀取):作用于主內存變量,表示把一個變量值從主內存傳輸到線程的工作內存中,以便下一步的 load 操作使用。
- load(載入):作用于線程的工作內存的變量,表示把 read 操作從主內存中讀取的變量值放到工作內存的變量副本中(副本是相對于主內存的變量而言的)。
- use(使用):作用于線程的工作內存中的變量,表示把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時就會執行該操作。
- assign(賦值):作用于線程的工作內存的變量,表示把執行引擎返回的值賦值給工作內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時就會執行該操作。
- store(存儲):作用于線程的工作內存中的變量,把工作內存中的一個變量的值傳遞給主內存,以便下一步的 write 操作使用。
- write(寫入):作用于主內存的變量,表示把 store 操作從工作內存中得到的變量的值放入主內存的變量中。
- unlock(解鎖):作用于主內存的變量,表示把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
JMM 還規定了以上八種操作需按照如下規則進行:
- 不允許read 和 load、store 和 write 操作之一單獨出現,也就是 read 操作后必須 load,store 操作后必須 write。即不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現。
- 不允許線程丟棄它最近的 assign 操作,即變量在工作內存中改變了之后必須把該變化同步回主內存。
- 不允許線程將沒有 assign 的數據從工作內存同步到主內存。
- 一個新的變量必須在主內存中誕生,不允許工作內存直接使用一個未被初始化的變量。也就是對變量實施 use 和 store 操作之前,必須經過 load 和 assign 操作。
- 一個變量同一時間只能有一個線程對其進行 lock 操作。但 lock 操作可以被同一條線程重復執行多次,多次 lock 之后,必須執行相同次數 unlock 才可以解鎖。
- 如果對一個變量進行 lock 操作,會清空所有工作內存中此變量的值。在執行引擎使用這個變量前,必須重新 load 或 assign 操作初始化變量的值。
- 如果一個變量沒有被 lock,就不能對其進行 unlock 操作。也不能 unlock 一個被其他線程鎖住的變量。
- 一個線程對一個變量進行 unlock 操作之前,必須先把此變量同步回主內存。
JMM 三大特征
JMM 三大特征分別是:原子性,可見性,有序性。整個 JMM 實際上也是圍繞著這三個特征建立起來的,并且也是 Java 并發編程的基礎。
原子性
原子性是指一個操作是不可分割、不可中斷的,要么全部執行成功要么全部執行失敗。
JMM 只能保證對基本數據類型的變量的讀寫操作是原子性的,但 long 和 double 除外(long 和 double 的非原子性協定)。
我們來看看下面的例子:
int x = 1;
int y = x;
x ++;
上面三行代碼只有第一行是原子性操作,基本類型賦值操作,必定是原子性操作。
第二行代碼先讀取 x 變量的值,再進行賦值給 y 變量,進行了兩個操作,不能保證原子性。
第三行代碼先讀取 x 變量的值,再進行加 1,最后再賦值給 x 變量,進行了三個操作,不能保證原子性。
在并發環境下,為了保證原子性,Java 提供了 synchronized 關鍵字。因此在 synchronized 修飾的代碼塊之間的操作都是原子性的。
可見性
可見性是指所有線程都能看到共享內存的最新狀態。即當一個線程修改了一個共享變量的值時,其他線程能夠立即看到該變量的最新值。
對于可見性問題,Java 是提供了一個 volatile 關鍵字來保證可見性。當一個共享變量被 volatile 關鍵字修飾時,這個變量被修改后會立即刷新到主內存,保證其他線程看到的值一定是最新的。
除了 volatile 關鍵字之外,final 和 synchronized 也能實現可見性。
final 關鍵字修飾的變量,在構造器中一旦初始化完成,如果沒有對象逸出(指對象沒有初始化完成就可以被別的線程使用),那么其他線程都就可以看見 final 修飾的變量。
synchronized 的原理是,線程進入 synchronized 代碼塊后,線程會獲取到 lock,將會清空本地內存,然后從主內存中拷貝共享變量的最新值到本地內存作為副本,執行代碼,又將修改后的副本值刷新到主內存中,最后線程執行 unlock。
有序性
有序性是指程序執行的順序按照代碼的先后順序執行。
在 Java 中,可以通過 volatile 和 synchronized 關鍵字來保證多線程之間操作的有序性。
volatile 關鍵字是通過在主存中加入內存屏障來達到禁止指令重排序,來保證有序性。
synchronized 關鍵字原理是,一個變量在同一時刻只能被一個線程 lock,并且必須 unlock 后,其他線程才可以重新 lock,使得被 synchronized 修飾的代碼塊在多線程之間是串行執行的。
【編輯推薦】
【責任編輯:姜華 TEL:(010)68476606】