上次線程池已經(jīng)說過了,從今天開始一起了解下JVM內(nèi)存模型詳解。
(一)容易誤解的部分
老鐵很容易把JAVA的內(nèi)存區(qū)域、JAVA的內(nèi)存模型,GC分代回收的老年代和新生代也容易搞混富弦,繞進去繞不出來。學習多線程之前一定要搞明白這些問題氛驮,可能在你的內(nèi)心一直認為多線程就是一個工具腕柜,所有的底層都是C++來寫的,沒辦法去看,為什么要有java盏缤,java其實就是屏蔽了底層的復雜性砰蠢。
- ① GC內(nèi)存區(qū)域
堆的概念,老年代唉铜,新生代台舱,Eden,S0潭流,S1
- ② JAVA的內(nèi)存區(qū)域
JVM運行時的區(qū)域:java編譯生成class竞惋,線程共享部分(方法區(qū),堆內(nèi)存)灰嫉,線程獨占部分(虛擬機棧拆宛,本地方法棧,程序計數(shù)器)
- ③ JAVA的內(nèi)存模型(概念)
針對多核多CPU讼撒,多線程而制定的一套規(guī)范規(guī)則浑厚,不是一種開發(fā)技術(shù)。
(二)多線程中的問題
- 所見非所得(你看到的并不是所想的)根盒、
- 無法肉眼去檢測程序的準確性(多線程下钳幅,完全看不出來正常不正常)。
- 不同的運行平臺有不同的表現(xiàn)郑象。
- 錯誤很難重現(xiàn)贡这。
(三)工作內(nèi)存和主內(nèi)存
- ① 主內(nèi)存
創(chuàng)建一個對象在堆里面茬末,也可以稱之為主內(nèi)存厂榛,不僅僅是在堆,存在一個對象X丽惭,就存在主內(nèi)存
- ② 工作內(nèi)存
線程運行在工作內(nèi)存击奶, 虛擬機棧,程序計數(shù)器责掏,CPU柜砾,高速緩存。
工作內(nèi)存和主內(nèi)存只是一個邏輯上的劃分换衬,概念上的東西痰驱。
- ③ 奇妙的現(xiàn)象
主內(nèi)存的flag傳輸?shù)焦ぷ鲀?nèi)存flag的時候,存在CPU緩存的情況瞳浦,CPU緩存可能導致非常短的時間內(nèi)不一致担映,本身CPU廠家底層是要做一致處理的,但是存在短時間內(nèi)的不一致叫潦。
(四)指令重排
- ① 介紹
Java語言規(guī)范JVM線程內(nèi)部維持順序或語義蝇完,即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼邏輯順序不一致,這個過程就叫做指令的重排序短蜕。
- ② 意義
使指令更加符合CPU的執(zhí)行特性氢架,最大限度的發(fā)揮機器的性能,提高程序的執(zhí)行效率朋魔。
重排序岖研,只能保證單個線程的,如果是多線程的話警检,就沒有爆發(fā)保證重排序缎玫。
// 線程1
a = d; b = 2
// 線程2
c = a; d =3
//重排序后
//線程1
b = 2 ; a =d;
//線程2
d = 3 ; c =a;
編譯器和處理器可能會對操作做重排序。編譯器和處理器在重排序時解滓,會遵守數(shù)據(jù)依賴性赃磨,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序。
(五) 如何不進行指令排序
- ① 介紹
The Java volatile keyword is used to mark a Java variable as "being stored in main memory". More precisely that means, that every read of a volatile variable will be read from the computer's main memory, and not from the CPU cache, and that every write to a volatile variable will be written to main memory, and not just to the CPU cache洼裤。 CPU不緩存邻辉。
- ② 實例
public class VisibilityDemo2 {
// 狀態(tài)標識 (不用緩存)
private volatile boolean flag = true;
// 源碼 -> 字節(jié)碼class
// JVM 轉(zhuǎn)化為 操作系統(tǒng)能夠執(zhí)行的代碼 (JIT Just In Time Compiler 編譯器 )(JVM -- client , --server)
public static void main(String[] args) throws InterruptedException {
VisibilityDemo2 demo1 = new VisibilityDemo2();
new Thread(new Runnable() {
public void run() {
int i = 0;
while (demo1.flag) {
i++;
}
System.out.println(i);
}
}).start();
TimeUnit.SECONDS.sleep(2);
// 設(shè)置is為false腮鞍,使上面的線程結(jié)束while循環(huán)
demo1.flag = false;
System.out.println("被置為false了.");
}
}
不添加volatile 值骇,就不會打印i的值。
(六) 內(nèi)存模型
- ① 介紹
內(nèi)存模型描述程序的可能行為移国。JAVA編程語言內(nèi)存模型通過檢查執(zhí)行跟蹤中的每個讀操作吱瘩,并根據(jù)某些規(guī)則檢查該操作觀察到的寫操作是否有效來工作。
只要程序的所有執(zhí)行產(chǎn)生的結(jié)果都可以由內(nèi)存模型預測迹缀,具體的實現(xiàn)者任意實現(xiàn)使碾,包括操作的重新排序和刪除不必要的同步。
內(nèi)存模型決定了在程序的每個點上可以讀取什么值祝懂。
- ② 共享變量描述
可以在線程之間共享的內(nèi)存稱為共享內(nèi)存或堆內(nèi)存票摇。所有實例字段,靜態(tài)字段和數(shù)組元素都存儲在堆內(nèi)存中砚蓬。如果至少有一個訪問是寫的矢门,那么對同一個變量的兩次訪問(讀或?qū)懀┦菦_突的。線程1修改過共享變量后灰蛙,將共享變量刷到主內(nèi)存祟剔,然后,線程2從主內(nèi)存讀取該共享變量摩梧,將該共享變量載入到工作內(nèi)存中物延。
- ③ 線程操作的定義
- write要寫的變量以及要寫的值。
- read 要讀的變量以及可見的寫入值(由此障本,我們可以確定可見的值)教届。
- lock 要鎖定的管程响鹃。
- unlock 要解鎖的管程。
- 外部操作(socket等等)案训。
- 啟動和終止买置。
如果一個程序沒有數(shù)據(jù)競爭,那么程序的所有執(zhí)行看起來都是順序一致的强霎。這是重排序必須要遵守的規(guī)則忿项。
(七)對于同步規(guī)則的定義
- ① 對于監(jiān)視器m的解鎖與所有后續(xù)操作對于m的加鎖同步
synchronized 在同步關(guān)鍵字,在內(nèi)存中都明確定義了城舞,保持可見轩触,及時反饋給主內(nèi)存中。一環(huán)扣一環(huán)家夺,想可見脱柱,必須反饋到主內(nèi)存。既有同步的語義拉馋,還有保持可見性的功能榨为。
import java.util.concurrent.TimeUnit;
public class VisibilityDemo1 {
// 狀態(tài)標識
private static boolean is = true;
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
int i = 0;
while (VisibilityDemo1.is) {
synchronized (this) {
i++;
}
}
System.out.println(i);
}
}).start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 設(shè)置is為false,使上面的線程結(jié)束while循環(huán)
VisibilityDemo1.is = false;
System.out.println("被置為false了.");
}
}
- ② 對volatile 變量v的寫入煌茴,與所有其他線程后續(xù)對 v 的讀同步
變量標識了volatile 随闺,后面不管哪個線程來讀,都是同步的蔓腐,都是可見的矩乐。
public class VisibilityDemo {
private volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
VisibilityDemo demo1 = new VisibilityDemo();
Thread thread1 = new Thread(new Runnable() {
public void run() {
int i = 0;
// class -> 運行時jit編譯 -> 匯編指令 -> 重排序
while (demo1.flag) { // 指令重排序
i++;
}
System.out.println(i);
}
});
thread1.start();
TimeUnit.SECONDS.sleep(2);
// 設(shè)置is為false,使上面的線程結(jié)束while循環(huán)
demo1.flag = false;
System.out.println("被置為false了.");
}
}
- ③ 啟動線程的操作與線程中的第一個操作同步
④ 對于每個屬性寫入默認值(0回论,false散罕,null)與 每個線程對其操作的同步
⑤ 線程T1的最后操作與線程T2發(fā)現(xiàn)線程T1已經(jīng)結(jié)束同步(isAlive,join可以判斷線程是否終結(jié))
⑥ 如果線程T1終端了T2,那么線程T1的中斷操作與其他所有線程發(fā)現(xiàn)T2倍中斷了同步透葛,通過拋出InterruptedException異常笨使,或者調(diào)用Thread.interrupted 或者 Thread.isInterrupted。
(八)Happyens-before先行發(fā)生原則
- ① 介紹
強調(diào)兩個有沖突的動作之間的順序僚害,以及定義數(shù)據(jù)征用的發(fā)生時機。
- ② 原則
- 同一個線程里面對數(shù)據(jù)做了變動繁调,后面的動作可以及時的看到萨蚕,其實還是可見性。
- 某個monitor上的unlock動作 happens-before 同一個monitor上后續(xù)的lock動作蹄胰。
- 對某個volatile 字段的寫操作 happens-before 每個后續(xù)對該 volatile 字段的讀操作岳遥。
- 在某個線程對象上調(diào)用start() 方法 happens-before 該啟動了的線程中的任意動作。
- 某個線程中的所有動作 happens-before 任意其他線程成功從該線程對象上的join() 中返回裕寨。
- 如果某個動作 a 在happens-before 動作 b浩蓉,b 在happens-before 動作 c派继,則 a happens-before c。
(九) final 在JMM中的處理
- ① final在該對象的構(gòu)造函數(shù)中設(shè)置對象的字段捻艳,當線程看到該對象時驾窟,將始終看到該對象的final字段的正確構(gòu)造版本。
② 如果在構(gòu)造函數(shù)中設(shè)置字段后發(fā)生讀取认轨,則會看到該final字段分配的值绅络,否則它將看到默認值。
③ 讀取該共享對象的final成員變量之前嘁字,先要讀取共享對象恩急。
④ 通常static final 是不可以修改的字段。然而System.in, System.out 和 System.err 是static final 字段纪蜒,遺留原因衷恭,必須允許通過set方法改變,這些字段稱為寫保護纯续,以區(qū)別于普通的final字段匾荆。
PS:使用了volatile,unlock和lock的時候杆烁,就可以保證代碼不進行重排序牙丽。內(nèi)存模型java進階的一個核心點,這個理解了兔魂,其實比寫多少年的業(yè)務代碼要重要很多烤芦。