摘要:本文的目的來理解 J V M 與我們的記憶體兩者之間是如何協調工作的。
本文分享自華為雲社群《一文帶你圖解Java記憶體模型》,作者: 龍哥手記 。
我們今天要特別重點講的,也就是我們本文的目的來理解 J V M 與我們的記憶體兩者之間是如何協調工作的,它的名字就是Java記憶體模型(JMM)。
一 打牢基礎
原子性是一種按原子方式的操作,那你有可能問了“原子方式”是啥?就是不可中斷的意思。你也可以理解不能再分。要麼不執行,要麼用原子的方式來執行,在這個過程中是不會被其他執行緒中斷。
有什麼栗子嗎?
眼見為實
class Data{ AtomicInteger atomicInteger = new AtomicInteger(); volatile int number=0; public void numberIncrement(){ this.number++; } public void atomicIntegerIncrement(){ this.atomicInteger.incrementAndGet(); } } public class Main { public static void main(String[] args) { Data data = new Data(); for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { data.numberIncrement(); data.atomicIntegerIncrement(); } },"t"+i).start(); } while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println("volatile修飾的int type:"+data.number); System.out.println("原子類:"+data.atomicInteger); } }
再看下不是原子性的案例
class Data{ volatile int number=0; public void numberIncrement(){ this.number++; } } public class Main { public static void main(String[] args) { Data data = new Data(); for (int i = 0; i < 10; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { data.numberIncrement(); } },"t"+i).start(); } while (Thread.activeCount() > 2){ Thread.yield(); } System.out.println(data.number); } }
這個程式目的是 10 個執行緒把 number 變為 10000,因為 volatile 不保證原子性,所以是達不到效果的.輸出結果如下:
這兩操作是原子性的,也就是順序執行且不能被打斷的,要麼都執行成功,要麼都失敗
可見性是執行緒對共享變數修改的可見狀態。假如一個執行緒修改了一個共享變數的值,其他執行緒立馬知道共享變數改了。比較好的例子就是 volatile 變數了。這裡敘述下大致的原理:
首先你的 volatile 變數對所有的執行緒都是可見的,指的是你執行完 assign 之後立即就會把共享變數複製到主記憶體上去;在其他任意一個執行緒讀取主記憶體物件時候,讀取都是存到自己的執行緒私有記憶體裡面,它是都會重新整理主記憶體。這僅僅是針對同一個執行緒,在主記憶體上是表現資料一致性的。但是那如果是其他執行緒的私有記憶體它們一起來存取到各其他執行緒的私有記憶體,那你私有記憶體和你的主記憶體的資料那可就不一定相同啊。這就是 volatile 它是不能保證啥?不能保證執行緒安全的。
怎麼樣讓它執行緒安全呢?
- 第一個條件:運算結果並不依賴變數的當前值,或者你能保證只有一個執行緒修改變數的值,就是上面我說的第一種情況。
- 第二個條件:變數不需要和其它的狀態變數共同參與不變約束。
最後一個有序性意思說如果在本執行緒內觀察,所有的操作都是有序的,說明執行緒間的操作具有有序性。那肯定有無序的,我們可以用java為我們提供好的 volatile 和 synchronized 兩個關鍵字來保證執行緒之間操作有序就完成。
先來回顧下指令重排序
因為在JVM內部,我們為了提高效能,編譯器和處理器會對指令做重排序,但是JMM確保在不同的編譯器和不同的處理器平臺之上,通過插入特定型別的 Memory Barrier,
有序性是指:按照程式碼的既定順序執行。
說的通俗一點,就是程式碼會按照指定的順序執行,例如,按照程式編寫的順序執行,先執行第一行程式碼,再執行第二行程式碼,然後是第三行程式碼,以此類推。如下圖所示。
指令重排序 編譯器或者直譯器為了優化程式的執行效能,有時會改變程式的執行順序。但是,編譯器或者直譯器對程式的執行順序進行修改,可能會導致意想不到的問題!
在單執行緒下,指令重排序可以保證最終執行的結果與程式順序執行的結果一致,但是在多執行緒下就會存在問題。
如果發生了指令重排序,則程式可能先執行第一行程式碼,再執行第三行程式碼,然後執行第二行程式碼,如下所示。
資料依賴性
如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,
好了我們要先整明白它有啥用?
它規定了一個執行緒如何並且能夠及時看到其他執行緒修改過後的變數的值,及如何到記憶體去同步我們們的共享變數。
happens-before先行發生原則
它用於描述兩個操作在記憶體中的可見性,這樣可以判斷資料是否存在競爭,執行緒是否安全的主要根據。
int a = 10; b = b + 1;
CPU有時候會為了計算單元的利用率將其進行指令重排,如果b = b + a 就不會進行指令重排,因為b的結果依賴於 a 的值。
二 JVM對記憶體模型的實現
在JVM內部,記憶體模型大致分為兩大塊:執行緒棧區和堆。如圖:
JVM中執行的每個執行緒都有自己的執行緒棧,執行緒棧包含了當前執行緒執行的方法呼叫相關資訊,我們也可以叫它呼叫棧。
從上圖得出,執行緒A和執行緒B之間如果要通訊的話,必須要經歷下面2個步驟:
首先,執行緒A裡面已更新的共享變數重新整理到主記憶體裡面去。 然後,執行緒B到主記憶體去讀取執行緒A之前已更新過的共享變數。
畫圖說明這兩個步驟:
本地記憶體A和B有主記憶體中共享變數x的副本。假設初始時,這三個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(我們先假設值為1)臨時存放在自己的本地記憶體A中。假如它們兩個需要通訊了,執行緒A首先把自己本地記憶體的x值變成了1。隨後,執行緒B到主記憶體中讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變成了1。
它是咋來的呢?
JVM規範由它來定義這玩意,你想嗎,記憶體模型,記憶體模型,就是告訴你在JVM中你的記憶體是如何分佈的。根據它特有的結構,就它的結構自然而然的表示出來它的功能。它的結構,我們先瞄一眼
看到上面圖沒有,小夥伴們先回憶概念:
Heap
優點:執行時資料區,動態分配記憶體大小,有 gc; 缺點:因為要在執行時動態分配記憶體,所以它的存取速度比棧要慢一些,物件是放在堆上,靜態型別和那個類的定義也是一起儲存在堆上的。
stack
優點:存取速度比 Heap 快,但是肯定比暫存器要慢一丟丟。 缺點:由於是JVM提前劃分好的,那它的資料大小和生命週期那就是確定的了,說明缺乏靈活性,你想你下有哪些用到的型別它的大小是固定的呢!莫錯,基本資料型別,那就多得很。(譬如char, boolean, double, int等,提示一下物件控制程式碼也屬於基本型別變數的哦)。
當一個執行緒去訪問一個物件時, 可以去訪問物件的成員變數, 如果有兩個執行緒訪問物件的成員變數,則每個執行緒都有物件的成員變數的私有拷貝。
讀完你也許一臉懵逼,這是啥?
正如上面講到的,Java記憶體模型和硬體記憶體結構並不一致。硬體記憶體裡面沒有區分堆和棧,