簡介:
一篇文章搞懂java記憶體模型、JMM三大特征、volatile關鍵字
思維導圖

文章已收錄Github精選,歡迎Star:https://github.com/yehongzhi/learningSummary
面試官:講講什麼是JMM
你要是整這個我可就不困了。
JMM就是Java記憶體模型(java memory model)。因為在不同的硬體生産商和不同的作業系統下,記憶體的通路有一定的差異,是以會造成相同的代碼運作在不同的系統上會出現各種問題。是以java記憶體模型(JMM)屏蔽掉各種硬體和作業系統的記憶體通路差異,以實作讓java程式在各種平台下都能達到一緻的并發效果。
Java記憶體模型規定所有的變量都存儲在主記憶體中,包括執行個體變量,靜态變量,但是不包括局部變量和方法參數。每個線程都有自己的工作記憶體,線程的工作記憶體儲存了該線程用到的變量和主記憶體的副本拷貝,線程對變量的操作都在工作記憶體中進行。線程不能直接讀寫主記憶體中的變量。
不同的線程之間也無法通路對方工作記憶體中的變量。線程之間變量值的傳遞均需要通過主記憶體來完成。
如果聽起來抽象的話,我可以畫張圖給你看看,會直覺一點:
每個線程的工作記憶體都是獨立的,線程操作資料隻能在工作記憶體中進行,然後刷回到主存。這是 Java 記憶體模型定義的線程基本工作方式。
溫馨提醒一下,這裡有些人會把Java記憶體模型誤解為Java記憶體結構,然後答到堆,棧,GC垃圾回收,最後和面試官想問的問題相差甚遠。實際上一般問到Java記憶體模型都是想問多線程,Java并發相關的問題。
面試官:那JMM定義了什麼
這個簡單,整個Java記憶體模型實際上是圍繞着三個特征建立起來的。分别是:原子性,可見性,有序性。這三個特征可謂是整個Java并發的基礎。
原子性
原子性指的是一個操作是不可分割,不可中斷的,一個線程在執行時不會被其他線程幹擾。
面試官拿筆寫了段代碼,下面這幾句代碼能保證原子性嗎?
int i = 2;int j = i;
i++;
i = i + 1;
第一句是基本類型指派操作,必定是原子性操作。
第二句先讀取i的值,再指派到j,兩步操作,不能保證原子性。
第三和第四句其實是等效的,先讀取i的值,再+1,最後指派到i,三步操作了,不能保證原子性。
JMM隻能保證基本的原子性,如果要保證一個代碼塊的原子性,提供了monitorenter 和 moniterexit 兩個位元組碼指令,也就是 synchronized 關鍵字。是以在 synchronized 塊之間的操作都是原子性的。
可見性
可見性指當一個線程修改共享變量的值,其他線程能夠立即知道被修改了。Java是利用volatile關鍵字來提供可見性的。 當變量被volatile修飾時,這個變量被修改後會立刻重新整理到主記憶體,當其它線程需要讀取該變量時,會去主記憶體中讀取新值。而普通變量則不能保證這一點。
除了volatile關鍵字之外,final和synchronized也能實作可見性。
synchronized的原理是,在執行完,進入unlock之前,必須将共享變量同步到主記憶體中。
final修飾的字段,一旦初始化完成,如果沒有對象逸出(指對象為初始化完成就可以被别的線程使用),那麼對于其他線程都是可見的。
有序性
在Java中,可以使用synchronized或者volatile保證多線程之間操作的有序性。實作原理有些差別:
volatile關鍵字是使用記憶體屏障達到禁止指令重排序,以保證有序性。
synchronized的原理是,一個線程lock之後,必須unlock後,其他線程才可以重新lock,使得被synchronized包住的代碼塊在多線程之間是串行執行的。
面試官:給我講一下八種記憶體互動操作吧
好的,面試官,記憶體互動操作有8種,我畫張圖給你看吧:
- lock(鎖定),作用于主記憶體中的變量,把變量辨別為線程獨占的狀态。
- read(讀取),作用于主記憶體的變量,把變量的值從主記憶體傳輸到線程的工作記憶體中,以便下一步的load操作使用。
- load(加載),作用于工作記憶體的變量,把read操作主存的變量放入到工作記憶體的變量副本中。
- use(使用),作用于工作記憶體的變量,把工作記憶體中的變量傳輸到執行引擎,每當虛拟機遇到一個需要使用到變量的值的位元組碼指令時将會執行這個操作。
- assign(指派),作用于工作記憶體的變量,它把一個從執行引擎中接受到的值指派給工作記憶體的變量副本中,每當虛拟機遇到一個給變量指派的位元組碼指令時将會執行這個操作。
- store(存儲),作用于工作記憶體的變量,它把一個從工作記憶體中一個變量的值傳送到主記憶體中,以便後續的write使用。
- write(寫入):作用于主記憶體中的變量,它把store操作從工作記憶體中得到的變量的值放入主記憶體的變量中。
- unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
我再補充一下JMM對8種記憶體互動操作制定的規則吧:
- 不允許read、load、store、write操作之一單獨出現,也就是read操作後必須load,store操作後必須write。
- 不允許線程丢棄他最近的assign操作,即工作記憶體中的變量資料改變了之後,必須告知主存。
- 不允許線程将沒有assign的資料從工作記憶體同步到主記憶體。
- 一個新的變量必須在主記憶體中誕生,不允許工作記憶體直接使用一個未被初始化的變量。就是對變量實施use、store操作之前,必須經過load和assign操作。
- 一個變量同一時間隻能有一個線程對其進行lock操作。多次lock之後,必須執行相同次數unlock才可以解鎖。
- 如果對一個變量進行lock操作,會清空所有工作記憶體中此變量的值。在執行引擎使用這個變量前,必須重新load或assign操作初始化變量的值。
- 如果一個變量沒有被lock,就不能對其進行unlock操作。也不能unlock一個被其他線程鎖住的變量。
- 一個線程對一個變量進行unlock操作之前,必須先把此變量同步回主記憶體。
面試官:講一下volatile關鍵字吧
内心:這可以重頭戲呀,可不能出岔子~
很多并發程式設計都使用了volatile關鍵字,主要的作用包括兩點:
- 保證線程間變量的可見性。
- 禁止CPU進行指令重排序。
volatile修飾的變量,當一個線程改變了該變量的值,其他線程是立即可見的。普通變量則需要重新讀取才能獲得最新值。
volatile保證可見性的流程大概就是這個一個過程:
volatile一定能保證線程安全嗎
先說結論吧,volatile不能一定能保證線程安全。
怎麼證明呢,我們看下面一段代碼的運作結果就知道了:
/**
* @author Ye Hongzhi 公衆号:java技術愛好者
**/public class VolatileTest extends Thread {private static volatile int count = 0;public static void main(String[] args) throws Exception {
Vector<Thread> threads = new Vector<>();for (int i = 0; i < 100; i++) {
VolatileTest thread = new VolatileTest();
threads.add(thread);
thread.start();
}//等待子線程全部完成for (Thread thread : threads) {
thread.join();
}//輸出結果,正确結果應該是1000,實際卻是984System.out.println(count);//984}@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {//休眠500毫秒Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
count++;
}
}
}
為什麼volatile不能保證線程安全?
很簡單呀,可見性不能保證操作的原子性,前面說過了count++不是原子性操作,會當做三步,先讀取count的值,然後+1,最後指派回去count變量。需要保證線程安全的話,需要使用synchronized關鍵字或者lock鎖,給count++這段代碼上鎖:
private static synchronized void add() {
count++;
}
禁止指令重排序
首先要講一下as-if-serial語義,不管怎麼重排序,(單線程)程式的執行結果不能被改變。
為了使指令更加符合CPU的執行特性,最大限度的發揮機器的性能,提高程式的執行效率,隻要程式的最終結果與它順序化情況的結果相等,那麼指令的執行順序可以與代碼邏輯順序不一緻,這個過程就叫做指令的重排序。
重排序的種類分為三種,分别是:編譯器重排序,指令級并行的重排序,記憶體系統重排序。整個過程如下所示:
指令重排序在單線程是沒有問題的,不會影響執行結果,而且還提高了性能。但是在多線程的環境下就不能保證一定不會影響執行結果了。
是以在多線程環境下,就需要禁止指令重排序。
volatile關鍵字禁止指令重排序有兩層意思:
- 當程式執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見,在其後面的操作肯定還沒有進行。
- 在進行指令優化時,不能将在對volatile變量通路的語句放在其後面執行,也不能把volatile變量後面的語句放到其前面執行。
下面舉個例子:
private static int a;//非volatile修飾變量private static int b;//非volatile修飾變量private static volatile int k;//volatile修飾變量private void hello() {
a = 1; //語句1b = 2; //語句2k = 3; //語句3a = 4; //語句4b = 5; //語句5//以下省略...}
變量a,b是非volatile修飾的變量,k則使用volatile修飾。是以語句3不能放在語句1、2前,也不能放在語句4、5後。但是語句1、2的順序是不能保證的,同理,語句4、5也不能保證順序。
并且,執行到語句3的時候,語句1,2是肯定執行完畢的,而且語句1,2的執行結果對于語句3,4,5是可見的。
volatile禁止指令重排序的原理是什麼
首先要講一下記憶體屏障,記憶體屏障可以分為以下幾類:
- LoadLoad 屏障:對于這樣的語句Load1,LoadLoad,Load2。在Load2及後續讀取操作要讀取的資料被通路前,保證Load1要讀取的資料被讀取完畢。
- StoreStore屏障:對于這樣的語句Store1, StoreStore, Store2,在Store2及後續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
- LoadStore 屏障:對于這樣的語句Load1, LoadStore,Store2,在Store2及後續寫入操作被刷出前,保證Load1要讀取的資料被讀取完畢。
- StoreLoad 屏障:對于這樣的語句Store1, StoreLoad,Load2,在Load2及後續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。
在每個volatile讀操作後插入LoadLoad屏障,在讀操作後插入LoadStore屏障。
在每個volatile寫操作的前面插入一個StoreStore屏障,後面插入一個SotreLoad屏障。
大概的原理就是這樣。
面試官:講得還不錯,基本上都講到了,時間也不早了,今天的面試就到這吧,回去等通知吧~
總結