原文:https://www.jianshu.com/p/9c5a7d21c02f
內(nèi)存模型與運(yùn)行時(shí)數(shù)據(jù)區(qū)
內(nèi)存模型
JAVA內(nèi)存模型簡(jiǎn)稱JMM(Java Memory Model ),定義了程序中各個(gè)共享變量的訪問規(guī)則。
Java Memory Model
變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程擁有自己的工作內(nèi)存用來存放變量的拷貝,線程的讀寫操作是在各自的工作內(nèi)存中進(jìn)行的,操作的對(duì)象都是變量的拷貝,操作完畢后在刷新到主內(nèi)存。
JMM規(guī)范定義了工作內(nèi)存和主內(nèi)存之間變量訪問的細(xì)節(jié),通過保障原子性、有序性、可見性實(shí)現(xiàn)線程安全。
運(yùn)行時(shí)數(shù)據(jù)區(qū)
運(yùn)行時(shí)數(shù)據(jù)區(qū)(JVM Runtime Data Areas)定義了JVM運(yùn)行期內(nèi)存的管理劃分。
JVM Runtime Data Areas
JVM在運(yùn)行時(shí)把內(nèi)存劃分成多個(gè)功能區(qū),每個(gè)區(qū)域?qū)?yīng)著不能的存儲(chǔ)內(nèi)容,生命周期,共享性質(zhì),GC策略等。
可以看到,能被線程共享的是方法區(qū)和堆中的數(shù)據(jù),也就是實(shí)例對(duì)象、數(shù)組和靜態(tài)變量,這些共享數(shù)據(jù)受到JMM規(guī)范影響。
而局部變量、方法參數(shù)、異常處理參數(shù)都在虛擬機(jī)棧中,這些數(shù)據(jù)為線程私有的,所以不受JMM規(guī)范影響。
原子性、可見性、有序性
原子性
原子操作是指一個(gè)操作不會(huì)被線程調(diào)度機(jī)制打斷,一旦開始,就一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程切換(context switch)。
原子性可以保障讀取到的某個(gè)屬性的值是由一個(gè)線程寫入的。 變量不會(huì)在同一時(shí)刻受到多個(gè)線程同時(shí)寫入造成干擾。如在32位的JVM中對(duì)64位long 或double值的寫操作是分成兩次相鄰的32位值寫操作,在多線程的環(huán)境下,可能會(huì)有線程只讀到了前32位,這種操作就是非原子性的,非原子性操作會(huì)受到多線程的干擾而產(chǎn)生結(jié)果混亂。
基本類型的單次讀寫操作是原子的,但是復(fù)合操作如:int i=0;i++,就是非原子性的。
JMM保障原子性的方法:volatile語(yǔ)義(保證變量單次操作的的原子性)、鎖語(yǔ)義。
可見性
共享內(nèi)存模型
可見性是指一個(gè)線程對(duì)變量的值進(jìn)行了修改,其他線程能夠立即得知這個(gè)修改。
如上圖:在共享內(nèi)存模型中如果有一個(gè)線程對(duì)變量i進(jìn)行了修改,在沒有可見性保障的情況下,其他兩個(gè)線程看到的i的值都是不確定的,變量i在數(shù)據(jù)爭(zhēng)用的情況下不具備不可見性。
可見性是保障多線程操作中數(shù)據(jù)一致性和結(jié)果正確性的基石,多線程環(huán)境下影響變量可見性的因素:
1、 指令重排序
2、 線程調(diào)度(切換)
3、 工作內(nèi)存和主內(nèi)存沒有及時(shí)刷新
JMM保障可見性的方法:fianl語(yǔ)義、volatile語(yǔ)義、鎖語(yǔ)義。
有序性
現(xiàn)代CPU的計(jì)算速度遠(yuǎn)遠(yuǎn)高于內(nèi)存的讀寫速度,CPU會(huì)采用高速緩存來抵消內(nèi)存訪問帶來的延遲。甚至高速緩存也分成多級(jí),最快的離CPU最近,但是其存取速度還是遠(yuǎn)遠(yuǎn)低于CUP指令執(zhí)行的速度,為了減少CACHE_WAIT,CPU會(huì)采用指令級(jí)并行重排序來提供執(zhí)行效率,也可以叫做CPU亂序執(zhí)行。
CUP的高速緩存與內(nèi)存之間不是實(shí)時(shí)同步的,高速緩與高速緩間也不是實(shí)時(shí)同步,而是通過緩存一致性協(xié)議(MESI)將數(shù)據(jù)新到主內(nèi)存,緩存和讀寫緩沖區(qū)之間也會(huì)通過指令重排序來優(yōu)化數(shù)據(jù)的刷新。
JIT編譯器也會(huì)在代碼編譯的時(shí)候?qū)Υa進(jìn)行重新整理,最大限度的去優(yōu)化代碼的執(zhí)行效率。
所以一段JAVA代碼從執(zhí)行到獲得結(jié)果,其執(zhí)行的順序其實(shí)是經(jīng)歷了2個(gè)階段三種重排序的優(yōu)化:
代碼重排序過程
保障重排序后結(jié)果正確性
1、as-if-serial語(yǔ)義
as-if-serial語(yǔ)義的意思指:所有的指令都可以為了優(yōu)化而被重排序,但是必須保證最終執(zhí)行的結(jié)果和重排序之前的結(jié)果是一致的,編譯器和處理器都會(huì)保證單線程下的as-if-serial語(yǔ)義。主要遵守的規(guī)則是重排序不破壞數(shù)據(jù)的依賴關(guān)系,如下圖,指令C依賴指令A(yù)和指令B,那么重排序只能在指令A(yù)和指令B之間發(fā)生。
數(shù)據(jù)依賴關(guān)系
as-if-serial語(yǔ)義保證了單線程環(huán)境下重排序之后程序執(zhí)行結(jié)果的正確性,JVM在單線程的情況下會(huì)遵as-if-serial語(yǔ)義,無(wú)需擔(dān)心重排序會(huì)干擾心內(nèi)存可見性。
2、hAppens-before原則
示例1
按照寫代碼的主觀意愿,可能期望是要么指令1先執(zhí)行,要么指令3先執(zhí)行,指令1先執(zhí)行就不應(yīng)該看到到指令4寫入的值,如果是指令3先執(zhí)行,就不應(yīng)該看到指令2寫入的值。
如果編譯器或者執(zhí)行CPU進(jìn)行了重排序,指令4在指令1前先執(zhí)行了,指令2在指令3之前執(zhí)行了,就會(huì)出現(xiàn)r2 == 2和r1 == 1這種有違直覺的結(jié)果。然而,從單個(gè)線程的角度,指令1和指令2重排序是遵循as-if-serial語(yǔ)義的,不會(huì)影響該線程獲得正確的結(jié)果。但是,從多線程的角度看,編譯器或者指令重排序影響到了代碼原本想要表達(dá)語(yǔ)義。
示例2
這個(gè)示例中指令1和指令2之間沒有依賴關(guān)系遵循as-if-serial語(yǔ)義重排序,對(duì)單線程執(zhí)行結(jié)果的正確性沒有影響,但是多線程環(huán)境下,如果thread1執(zhí)行完指令1,thread2執(zhí)行,那i的值會(huì)出現(xiàn)有背預(yù)期的情況,因?yàn)閠hread1中對(duì)共享變量a的修改,對(duì)thread2是不可見的。
基于數(shù)據(jù)依賴性的as-if-serial語(yǔ)義無(wú)法保證多線程環(huán)境下,重排序之后程序執(zhí)行結(jié)果的正確性。JMM中happens-before原則就是用來保障多線程環(huán)境下變量可見性的。
先行發(fā)生原則( happens-before )是JMM用來規(guī)定兩個(gè)操作之間的偏序關(guān)系,這兩個(gè)操作是可以跨線程的。happens-before中確定了8條規(guī)則,如果如果兩個(gè)操作之間的關(guān)系可以從下列規(guī)則推導(dǎo)出來說明兩個(gè)操作是有序的。
happens-before并不限定指令重排序,如果如果重排序之后的執(zhí)行結(jié)果與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么JVM允許這種重排序。happens-before原則保證了前后兩個(gè)操作間不會(huì)被重排序且后者對(duì)前者的內(nèi)存是可見的。
happens-before八條規(guī)則:
1、程序次序規(guī)則:一個(gè)線程中的每個(gè)操作,happens-before于該線程中的任意后續(xù)操作(一個(gè)線程內(nèi)保證語(yǔ)義的串行性)。
2、鎖定規(guī)則:對(duì)一個(gè)鎖的解鎖,happens-before于隨后對(duì)這個(gè)鎖的加鎖。
3、volatile變量規(guī)則:volatile變量的寫操作happens-before于后面對(duì)這個(gè)變量的讀操作。
4、傳遞規(guī)則:如果A happens-before B且Bhappens-before C,那么A happens-before C。
5、線程啟動(dòng)規(guī)則:Thread對(duì)象的start()方法happens-before于此線程的每個(gè)一動(dòng)作。
6、線程中斷規(guī)則:對(duì)線程interrupt()方法的調(diào)用happens-before于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生。
7、線程終結(jié)規(guī)則:線程中所有的操作都happens-before于線程的終止。
8、對(duì)象終結(jié)規(guī)則:一個(gè)對(duì)象的初始化完成happens-before于他的finalize()方法的開始。
小結(jié)
1、JMM規(guī)范定義了工作內(nèi)存和主內(nèi)存之間變量訪問的細(xì)節(jié),通過保障原子性、有序性、可見性實(shí)現(xiàn)線程安全。
2、線程調(diào)度(切換)會(huì)影響數(shù)據(jù)操作的原子性,JMM通過fianl語(yǔ)義、volatile語(yǔ)義、鎖語(yǔ)義來保障原子性。
3、線程調(diào)度(切換)、指令重排序、內(nèi)存刷新都會(huì)影響可見性,JMM通過volatile語(yǔ)義、鎖語(yǔ)義來保障可見性。
4、內(nèi)存系統(tǒng)重排序、指令級(jí)并行重排序、編譯器優(yōu)化重排序都會(huì)影響到程序執(zhí)行的有序性,JMM通過happens-before原則保障并發(fā)環(huán)境下程序執(zhí)行的有序性。