上次線程池已經(jīng)說過了,從今天開始一起了解下JVM內(nèi)存模型詳解。
(一)容易誤解的部分
老鐵很容易把JAVA的內(nèi)存區(qū)域、JAVA的內(nèi)存模型,GC分代回收的老年代和新生代也容易搞混,繞進(jìn)去繞不出來。學(xué)習(xí)多線程之前一定要搞明白這些問題,可能在你的內(nèi)心一直認(rèn)為多線程就是一個(gè)工具,所有的底層都是C++來寫的,沒辦法去看,為什么要有java,java其實(shí)就是屏蔽了底層的復(fù)雜性。
- ① GC內(nèi)存區(qū)域
堆的概念,老年代,新生代,Eden,S0,S1
- ② JAVA的內(nèi)存區(qū)域
JVM運(yùn)行時(shí)的區(qū)域:java編譯生成class,線程共享部分(方法區(qū),堆內(nèi)存),線程獨(dú)占部分(虛擬機(jī)棧,本地方法棧,程序計(jì)數(shù)器)
- ③ JAVA的內(nèi)存模型(概念)
針對(duì)多核多CPU,多線程而制定的一套規(guī)范規(guī)則,不是一種開發(fā)技術(shù)。
(二)多線程中的問題
- 所見非所得(你看到的并不是所想的)、
- 無法肉眼去檢測(cè)程序的準(zhǔn)確性(多線程下,完全看不出來正常不正常)。
- 不同的運(yùn)行平臺(tái)有不同的表現(xiàn)。
- 錯(cuò)誤很難重現(xiàn)。
(三)工作內(nèi)存和主內(nèi)存
- ① 主內(nèi)存
創(chuàng)建一個(gè)對(duì)象在堆里面,也可以稱之為主內(nèi)存,不僅僅是在堆,存在一個(gè)對(duì)象X,就存在主內(nèi)存
- ② 工作內(nèi)存
線程運(yùn)行在工作內(nèi)存, 虛擬機(jī)棧,程序計(jì)數(shù)器,CPU,高速緩存。
工作內(nèi)存和主內(nèi)存只是一個(gè)邏輯上的劃分,概念上的東西。
- ③ 奇妙的現(xiàn)象
主內(nèi)存的flag傳輸?shù)焦ぷ鲀?nèi)存flag的時(shí)候,存在CPU緩存的情況,CPU緩存可能導(dǎo)致非常短的時(shí)間內(nèi)不一致,本身CPU廠家底層是要做一致處理的,但是存在短時(shí)間內(nèi)的不一致。
(四)指令重排
- ① 介紹
Java語言規(guī)范JVM線程內(nèi)部維持順序或語義,即只要程序的最終結(jié)果與它順序化情況的結(jié)果相等,那么指令的執(zhí)行順序可以與代碼邏輯順序不一致,這個(gè)過程就叫做指令的重排序。
- ② 意義
使指令更加符合CPU的執(zhí)行特性,最大限度的發(fā)揮機(jī)器的性能,提高程序的執(zhí)行效率。
重排序,只能保證單個(gè)線程的,如果是多線程的話,就沒有爆發(fā)保證重排序。
// 線程1
a = d; b = 2
// 線程2
c = a; d =3
//重排序后
//線程1
b = 2 ; a =d;
//線程2
d = 3 ; c =a;
編譯器和處理器可能會(huì)對(duì)操作做重排序。編譯器和處理器在重排序時(shí),會(huì)遵守?cái)?shù)據(jù)依賴性,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
(五) 如何不進(jìn)行指令排序
- ① 介紹
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不緩存。
- ② 實(shí)例
public class VisibilityDemo2 {
// 狀態(tài)標(biāo)識(shí) (不用緩存)
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 ,就不會(huì)打印i的值。
(六) 內(nèi)存模型
- ① 介紹
內(nèi)存模型描述程序的可能行為。JAVA編程語言內(nèi)存模型通過檢查執(zhí)行跟蹤中的每個(gè)讀操作,并根據(jù)某些規(guī)則檢查該操作觀察到的寫操作是否有效來工作。
只要程序的所有執(zhí)行產(chǎn)生的結(jié)果都可以由內(nèi)存模型預(yù)測(cè),具體的實(shí)現(xiàn)者任意實(shí)現(xiàn),包括操作的重新排序和刪除不必要的同步。
內(nèi)存模型決定了在程序的每個(gè)點(diǎn)上可以讀取什么值。
- ② 共享變量描述
可以在線程之間共享的內(nèi)存稱為共享內(nèi)存或堆內(nèi)存。所有實(shí)例字段,靜態(tài)字段和數(shù)組元素都存儲(chǔ)在堆內(nèi)存中。如果至少有一個(gè)訪問是寫的,那么對(duì)同一個(gè)變量的兩次訪問(讀或?qū)懀┦菦_突的。線程1修改過共享變量后,將共享變量刷到主內(nèi)存,然后,線程2從主內(nèi)存讀取該共享變量,將該共享變量載入到工作內(nèi)存中。
- ③ 線程操作的定義
- write要寫的變量以及要寫的值。
- read 要讀的變量以及可見的寫入值(由此,我們可以確定可見的值)。
- lock 要鎖定的管程。
- unlock 要解鎖的管程。
- 外部操作(socket等等)。
- 啟動(dòng)和終止。
如果一個(gè)程序沒有數(shù)據(jù)競(jìng)爭(zhēng),那么程序的所有執(zhí)行看起來都是順序一致的。這是重排序必須要遵守的規(guī)則。
(七)對(duì)于同步規(guī)則的定義
- ① 對(duì)于監(jiān)視器m的解鎖與所有后續(xù)操作對(duì)于m的加鎖同步
synchronized 在同步關(guān)鍵字,在內(nèi)存中都明確定義了,保持可見,及時(shí)反饋給主內(nèi)存中。一環(huán)扣一環(huán),想可見,必須反饋到主內(nèi)存。既有同步的語義,還有保持可見性的功能。
import java.util.concurrent.TimeUnit;
public class VisibilityDemo1 {
// 狀態(tài)標(biāo)識(shí)
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了.");
}
}
- ② 對(duì)volatile 變量v的寫入,與所有其他線程后續(xù)對(duì) v 的讀同步
變量標(biāo)識(shí)了volatile ,后面不管哪個(gè)線程來讀,都是同步的,都是可見的。
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 -> 運(yùn)行時(shí)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了.");
}
}
- ③ 啟動(dòng)線程的操作與線程中的第一個(gè)操作同步
④ 對(duì)于每個(gè)屬性寫入默認(rèn)值(0,false,null)與 每個(gè)線程對(duì)其操作的同步
⑤ 線程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ā)生原則
- ① 介紹
強(qiáng)調(diào)兩個(gè)有沖突的動(dòng)作之間的順序,以及定義數(shù)據(jù)征用的發(fā)生時(shí)機(jī)。
- ② 原則
- 同一個(gè)線程里面對(duì)數(shù)據(jù)做了變動(dòng),后面的動(dòng)作可以及時(shí)的看到,其實(shí)還是可見性。
- 某個(gè)monitor上的unlock動(dòng)作 happens-before 同一個(gè)monitor上后續(xù)的lock動(dòng)作。
- 對(duì)某個(gè)volatile 字段的寫操作 happens-before 每個(gè)后續(xù)對(duì)該 volatile 字段的讀操作。
- 在某個(gè)線程對(duì)象上調(diào)用start() 方法 happens-before 該啟動(dòng)了的線程中的任意動(dòng)作。
- 某個(gè)線程中的所有動(dòng)作 happens-before 任意其他線程成功從該線程對(duì)象上的join() 中返回。
- 如果某個(gè)動(dòng)作 a 在happens-before 動(dòng)作 b,b 在happens-before 動(dòng)作 c,則 a happens-before c。
(九) final 在JMM中的處理
- ① final在該對(duì)象的構(gòu)造函數(shù)中設(shè)置對(duì)象的字段,當(dāng)線程看到該對(duì)象時(shí),將始終看到該對(duì)象的final字段的正確構(gòu)造版本。
② 如果在構(gòu)造函數(shù)中設(shè)置字段后發(fā)生讀取,則會(huì)看到該final字段分配的值,否則它將看到默認(rèn)值。
③ 讀取該共享對(duì)象的final成員變量之前,先要讀取共享對(duì)象。
④ 通常static final 是不可以修改的字段。然而System.in, System.out 和 System.err 是static final 字段,遺留原因,必須允許通過set方法改變,這些字段稱為寫保護(hù),以區(qū)別于普通的final字段。
PS:使用了volatile,unlock和lock的時(shí)候,就可以保證代碼不進(jìn)行重排序。內(nèi)存模型java進(jìn)階的一個(gè)核心點(diǎn),這個(gè)理解了,其實(shí)比寫多少年的業(yè)務(wù)代碼要重要很多。