java內存模型 JMM
575
2022-05-29
我們今天要特別重點講的,也就是我們本文的目的來理解 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中你的內存是如何分布的。根據它特有的結構,就它的結構自然而然的表示出來它的功能。它的結構,我們先瞄一眼
看到上面圖沒有,小伙伴們先回憶概念:
優點:運行時數據區,動態分配內存大小,有 gc; 缺點:因為要在運行時動態分配內存,所以它的存取速度比棧要慢一些,對象是放在堆上,靜態類型和那個類的定義也是一起存儲在堆上的。
優點:存取速度比 Heap 快,但是肯定比寄存器要慢一丟丟。 缺點:由于是JVM提前劃分好的,那它的數據大小和生命周期那就是確定的了,說明缺乏靈活性,你想你下有哪些用到的類型它的大小是固定的呢!莫錯,基本數據類型,那就多得很。(譬如char, boolean, double, int等,提示一下對象句柄也屬于基本類型變量的哦)。
當一個線程去訪問一個對象時, 可以去訪問對象的成員變量, 如果有兩個線程訪問對象的成員變量,則每個線程都有對象的成員變量的私有拷貝。
讀完你也許一臉懵逼,這是啥?
正如上面講到的,Java內存模型和硬件內存結構并不一致。硬件內存里面沒有區分堆和棧,
關于我
這里是小希,一個熱愛技術的Java程序猿,公眾號「碼工是小?!估飳ㄆ诜窒頂祿Y構,計算機網絡,Java,分布式,數據庫等精品原創文章,
非常感謝各位小哥哥小姐姐們能看到這里,原創不易,文章有幫助可以關注,點個贊,分享與評論,都是支持(莫要白嫖)!
堅持就會很酷
下期見
Java 云硬盤 EVS 任務調度 數據快遞服務 緩存
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。