Java內(nèi)存模型-jsr133規(guī)范介紹
最近在看《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》講到了線程相關(guān)的細(xì)節(jié)知識(shí),里面講述了關(guān)于java內(nèi)存模型,也就是jsr 133定義的規(guī)范。
系統(tǒng)的看了jsr 133規(guī)范的前面幾個(gè)章節(jié)的內(nèi)容,覺(jué)得受益匪淺。廢話不說(shuō),簡(jiǎn)要的介紹一下java內(nèi)存規(guī)范。
什么是內(nèi)存規(guī)范
在jsr-133中是這么定義的
A memory model describes, given a program and an execution trace of that program, whether
the execution trace is a legal execution of the program. For the Java programming language, the
memory model works by examining each read in an execution trace and checking that the write
observed by that read is valid according to certain rules.
也就是說(shuō)一個(gè)內(nèi)存模型描述了一個(gè)給定的程序和和它的執(zhí)行路徑是否一個(gè)合法的執(zhí)行路徑。對(duì)于java序言來(lái)說(shuō),內(nèi)存模型通過(guò)考察在程序執(zhí)行路徑中每一個(gè)讀操作,根據(jù)特定的規(guī)則,檢查寫(xiě)操作對(duì)應(yīng)的讀操作是否能是有效的。
java內(nèi)存模型只是定義了一個(gè)規(guī)范,具體的實(shí)現(xiàn)可以是根據(jù)實(shí)際情況自由實(shí)現(xiàn)的。但是實(shí)現(xiàn)要滿足java內(nèi)存模型定義的規(guī)范。
處理器和內(nèi)存的交互
這個(gè)要感謝硅工業(yè)的發(fā)展,導(dǎo)致目前處理器的性能越來(lái)越強(qiáng)大。目前市場(chǎng)上基本上都是多核處理器。如何利用多核處理器執(zhí)行程序的優(yōu)勢(shì),使得程序性能得到極大的提升,是目前來(lái)說(shuō)最重要的。
目前所有的運(yùn)算都是處理器來(lái)執(zhí)行的,我們?cè)诖髮W(xué)的時(shí)候就學(xué)習(xí)過(guò)一個(gè)基本概念 程序 = 數(shù)據(jù) + 算法 ,那么處理器負(fù)責(zé)計(jì)算,數(shù)據(jù)從哪里獲取了?
數(shù)據(jù)可以存放在處理器寄存器里面(目前x86處理都是基于寄存器架構(gòu)的),處理器緩存里面,內(nèi)存,磁盤(pán),光驅(qū)等。處理器訪問(wèn)這些數(shù)據(jù)的速度從快到慢依次為:寄存器,處理器緩存,內(nèi)存,磁盤(pán),光驅(qū)。為了加快程序運(yùn)行速度,數(shù)據(jù)離處理器越近越好。但是寄存器,處理器緩存都是處理器私有數(shù)據(jù),只有內(nèi)存,磁盤(pán),光驅(qū)才是才是所有處理器都可以訪問(wèn)的全局?jǐn)?shù)據(jù)(磁盤(pán)和光驅(qū)我們這里不討論,只討論內(nèi)存)如果程序是多線程的,那么不同的線程可能分配到不同的處理器來(lái)執(zhí)行,這些處理器需要把數(shù)據(jù)從主內(nèi)存加載到處理器緩存和寄存器里面才可以執(zhí)行(這個(gè)大學(xué)操作系統(tǒng)概念里面有介紹),數(shù)據(jù)執(zhí)行完成之后,在把執(zhí)行結(jié)果同步到主內(nèi)存。如果這些數(shù)據(jù)是所有線程共享的,那么就會(huì)發(fā)生同步問(wèn)題。處理器需要解決何時(shí)同步主內(nèi)存數(shù)據(jù),以及處理執(zhí)行結(jié)果何時(shí)同步到主內(nèi)存,因?yàn)橥粋€(gè)處理器可能會(huì)先把數(shù)據(jù)放在處理器緩存里面,以便程序后續(xù)繼續(xù)對(duì)數(shù)據(jù)進(jìn)行操作。所以對(duì)于內(nèi)存數(shù)據(jù),由于多處理器的情況,會(huì)變的很復(fù)雜。下面是一個(gè)例子:
初始值 a = b = 0
process1 process2
1:load a 5:load b
2:write a:2 6:add b:1
3:load b 7: load a
4:write b:1 8:write a:1
假設(shè)處理器1先加載內(nèi)存變量a,寫(xiě)入a的值為2,然后加載b,寫(xiě)入b的值為1,同時(shí) 處理2先加載b,執(zhí)行b+1,那么b在處理器2的結(jié)果可能是1 可能是3。因?yàn)樵趌oad b之前,不知道處理器1是否已經(jīng)吧b寫(xiě)會(huì)到主內(nèi)存。對(duì)于a來(lái)說(shuō),假設(shè)處理器1后于處理器2把a(bǔ)寫(xiě)會(huì)到主內(nèi)存,那么a的值則為2。
而內(nèi)存模型就是規(guī)定了一個(gè)規(guī)則,處理器如何同主內(nèi)存同步數(shù)據(jù)的一個(gè)規(guī)則。
內(nèi)存模型介紹
在介紹java內(nèi)存模型之前,我們先看看兩個(gè)內(nèi)存模型
Sequential Consistency Memory Model:連續(xù)一致性模型。這個(gè)模型定義了程序執(zhí)行的順序和代碼執(zhí)行的順序是一致的。也就是說(shuō) 如果兩個(gè)線程,一個(gè)線程T1對(duì)共享變量A進(jìn)行寫(xiě)操作,另外一個(gè)線程T2對(duì)A進(jìn)行讀操作。如果線程T1在時(shí)間上先于T2執(zhí)行,那么T2就可以看見(jiàn)T1修改之后的值。
這個(gè)內(nèi)存模型比較簡(jiǎn)單,也比較直觀,比較符合現(xiàn)實(shí)世界的邏輯。但是這個(gè)模型定義比較嚴(yán)格,在多處理器并發(fā)執(zhí)行程序的時(shí)候,會(huì)嚴(yán)重的影響程序的性能。因?yàn)槊看螌?duì)共享變量的修改都要立刻同步會(huì)主內(nèi)存,不能把變量保存到處理器寄存器里面或者處理器緩存里面。導(dǎo)致頻繁的讀寫(xiě)內(nèi)存影響性能。
Happens-Before Memory Model : 先行發(fā)生模型。這個(gè)模型理解起來(lái)就比較困難。先介紹一個(gè)現(xiàn)行發(fā)生關(guān)系 (Happens-Before Relationship)
如果有兩個(gè)操作A和B存在A Happens-Before B,那么操作A對(duì)變量的修改對(duì)操作B來(lái)說(shuō)是可見(jiàn)的。這個(gè)現(xiàn)行并不是代碼執(zhí)行時(shí)間上的先后關(guān)系,而是保證執(zhí)行結(jié)果是順序的。看下面例子來(lái)說(shuō)明現(xiàn)行發(fā)生
A,B為共享變量,r1,r2為局部變量 初始 A=B=0 Thread1 | Thread2 1: r2=A | 3: r1=B 2: B=2 | 4: A=2
憑借直觀感覺(jué),線程1先執(zhí)行 r2=A,則r2=0 ,然后賦值B=1,線程2執(zhí)行r1=B,由于線程1修改了B的值為1,所以r1=1。但是在現(xiàn)行發(fā)生內(nèi)存模型里面,有可能最終結(jié)果為r1 = r2 = 2。為什么會(huì)這樣,因?yàn)榫幾g器或者多處理器可能對(duì)指令進(jìn)行亂序執(zhí)行,線程1 從代碼流上面看是先執(zhí)行r2 = A,B = 1,但是處理器執(zhí)行的時(shí)候會(huì)先執(zhí)行 B = 2 ,在執(zhí)行 r2 = A,線程2 可能先執(zhí)行 A = 2 ,在執(zhí)行r1 = B,這樣可能 會(huì)導(dǎo)致 r1 = r2 = 2。
那我們先看看先行發(fā)生關(guān)系的規(guī)則
- 1 在同一個(gè)線程里面,按照代碼執(zhí)行的順序(也就是代碼語(yǔ)義的順序),前一個(gè)操作先于后面一個(gè)操作發(fā)生
- 2 對(duì)一個(gè)monitor對(duì)象的解鎖操作先于后續(xù)對(duì)同一個(gè)monitor對(duì)象的鎖操作
- 3 對(duì)volatile字段的寫(xiě)操作先于后面的對(duì)此字段的讀操作
- 4 對(duì)線程的start操作(調(diào)用線程對(duì)象的start()方法)先于這個(gè)線程的其他任何操作
- 5 一個(gè)線程中所有的操作先于其他任何線程在此線程上調(diào)用 join()方法
- 6 如果A操作優(yōu)先于B,B操作優(yōu)先于C,那么A操作優(yōu)先于C
解釋一下以上幾個(gè)先行發(fā)生規(guī)則的含義
規(guī)則1應(yīng)該比較好理解,因?yàn)楸容^適合人正常的思維。比如在同一個(gè)線程t里面,代碼的順序如下:
thread 1 共享變量A、B 局部變量r1、r2 代碼順序 1: A =1 2: r1 = A 3: B = 2 4: r2 = B 執(zhí)行結(jié)果 就是 A=1 ,B=2 ,r1=1 ,r2=2
因?yàn)橐陨鲜窃谕粋€(gè)線程里面,按照規(guī)則1 也就是按照代碼順序,A = 1 先行發(fā)生 r1 =A ,那么r1 = 1
再看規(guī)則2,下面是jsr133的例子

按照規(guī)則2,由于unlock操作先于發(fā)生于lock操作,所以X=1對(duì)線程2里面就是可見(jiàn)的,所以r2 = 1

在分析以下,看這個(gè)例子,由于unlock操作先于lock操作,所以線程x=1對(duì)于線程2不一定是可見(jiàn)(不一定是現(xiàn)行發(fā)生的),所以r2的值不一定是1,有可能是x賦值為1之前的那個(gè)狀態(tài)值(假設(shè)x初始值為0,那么此時(shí)r2的值可能為0)
對(duì)于規(guī)則3,我們可以稍微修改一下我們說(shuō)明的第一個(gè)例子
A,B為共享變量,并且B是valotile類型的 r1,r2為局部變量 初始 A=B=0 Thread1 | Thread2 1: r2=A | 3: r1=B 2: B=2 | 4: A=2 那么r1 = 2, r2可能為0或者2
因?yàn)閷?duì)于volatile類型的變量B,線程1對(duì)B的更新馬上線程2就是可見(jiàn)的,所以r1的值就是確定的。由于A是非valotile類型的,所以值不確定。
規(guī)則4,5,6這里就不解釋了,知道規(guī)則就可以了。
可以從以上的看出,先行發(fā)生的規(guī)則有很大的靈活性,編譯器可以對(duì)指令進(jìn)行重新排序,以便滿足處理器性能的需要。只要重新排序之后的結(jié)果,在單一線程里面執(zhí)行結(jié)果是可見(jiàn)的(也就是在同一個(gè)線程里面滿足先行發(fā)生原則1就可以了)。
java內(nèi)存模型是建立在先行發(fā)生的內(nèi)存模型之上的,并且再此基礎(chǔ)上,增強(qiáng)了一些。因?yàn)楝F(xiàn)行發(fā)生是一個(gè)弱約束的內(nèi)存模型,在多線程競(jìng)爭(zhēng)訪問(wèn)共享數(shù)據(jù)的時(shí)候,會(huì)導(dǎo)致不可預(yù)期的結(jié)果。有一些是java內(nèi)存模型可以接受的,有一些是java內(nèi)存模型不可以接受的。具體細(xì)節(jié)這里面就不詳細(xì)說(shuō)明了。這里只說(shuō)明關(guān)于java新的內(nèi)存模型重要點(diǎn)。
final字段的語(yǔ)義
在java里面,如果一個(gè)類定義了一個(gè)final屬性,那么這個(gè)屬性在初始化之后就不可以在改變。一般認(rèn)為final字段是不變的。在java內(nèi)存模型里面,對(duì)final有一個(gè)特殊的處理。如果一個(gè)類C定義了一個(gè)非static的final屬性A,以及非static final屬性B,在C的構(gòu)造器里面對(duì)A,B進(jìn)行初始化,如果一個(gè)線程T1創(chuàng)建了類C的一個(gè)對(duì)象co,同一時(shí)刻線程T2訪問(wèn)co對(duì)象的A和B屬性,如果t2獲取到已經(jīng)構(gòu)造完成的co對(duì)象,那么屬性A的值是可以確定的,屬性B的值可能還未初始化,
下面一段代碼演示了這個(gè)情況
public class FinalVarClass {
public final int a ;
public int b = 0;
static FinalVarClass co;
public FinalVarClass(){
a = 1;
b = 1;
}
//線程1創(chuàng)建FinalVarClass對(duì)象 co
public static void create(){
if(co == null){
co = new FinalVarClass();
}
}
//線程2訪問(wèn)co對(duì)象的a,b屬性
public static void vistor(){
if(co != null){
System.out.println(co.a);//這里返回的一定是1,a一定初始化完成
System.out.println(co.b);//這里返回的可能是0,因?yàn)閎還未初始化完成
}
}
}
為什么會(huì)發(fā)生這種情況,原因可能是處理器對(duì)創(chuàng)建對(duì)象的指令進(jìn)行重新排序。正常情況下,對(duì)象創(chuàng)建語(yǔ)句co = new FinalVarClass()并不是原子的,簡(jiǎn)單來(lái)說(shuō),可以分為幾個(gè)步驟,1 分配內(nèi)存空間 2 創(chuàng)建空的對(duì)象 3 初始化空的對(duì)象 4 把初始化完成的對(duì)象引用指向 co ,由于這幾個(gè)步驟處理器可能并發(fā)執(zhí)行,比如3,4 并發(fā)執(zhí)行,所以在create操作完成之后,co不一定馬上初始化完成,所以在vistor方法的時(shí)候,b的值可能還未初始化。但是如果是final字段,必須保證在對(duì)應(yīng)返回引用之前初始化完成。
volatile語(yǔ)義
對(duì)于volatile字段,在現(xiàn)行發(fā)生規(guī)則里面已經(jīng)介紹過(guò),對(duì)volatile變量的寫(xiě)操作先于對(duì)變量的讀操作。也就是說(shuō)任何對(duì)volatile變量的修改,都可以在其他線程里面反應(yīng)出來(lái)。對(duì)于volatile變量的介紹可以參考 本人寫(xiě)的一篇文章 《java中volatile關(guān)鍵字的含義》 里面有詳細(xì)的介紹。
volatile在java新的內(nèi)存規(guī)范里面還加強(qiáng)了新的語(yǔ)義。在老的內(nèi)存規(guī)范里面,volatile變量與非volatile變量的順序是可以重新排序的。舉個(gè)例子
public class VolatileClass {
int x = 0;
volatile boolean v = false;
//線程1write
public void writer() {
x = 42;
v = true;
}
//線程2 read
public void reader() {
if (v == true) {
System.out.println(x);//結(jié)果可能為0,可能為2
}
}
}
線程1先調(diào)用writer方法,對(duì)x和v進(jìn)行寫(xiě)操作,線程reader判斷,如果v=true,則打印x。在老的內(nèi)存規(guī)范里面,可能對(duì)v和x賦值順序發(fā)生改變,導(dǎo)致v的寫(xiě)操作先行于x的寫(xiě)操作執(zhí)行,同時(shí)另外一個(gè)線程判斷v的結(jié)果,由于v的寫(xiě)操作先行于v的讀操作,所以if(v==true)返回真,于是程序執(zhí)行打印x,此時(shí)x不一定先行與System.out.println指令之前。所以顯示的結(jié)果可能為0,不一定為2
但是java新的內(nèi)存模型jsr133修正了這個(gè)問(wèn)題,對(duì)于volatile語(yǔ)義的變量,自動(dòng)進(jìn)行l(wèi)ock 和 unlock操作包圍對(duì)變量volatile的讀寫(xiě)操作。那么以上語(yǔ)句的順序可以表示為
thread1 thread2
1 :write x=1 5:lock(m)
2 :lock(m) 6:read v
3 :write v=true 7:unlock(m)
4 :unlock 8 :if(v==true)
9: System.out.print(x)
由于unlock操作先于lock操作,所以x寫(xiě)操作5先于發(fā)生x的讀操作9
以上只是jsr規(guī)范中一些小結(jié)行的內(nèi)容,由于jsr133規(guī)范定義了很多術(shù)語(yǔ)以及很多推論,上述只是簡(jiǎn)單的介紹了一些比較重要的內(nèi)容,具體細(xì)節(jié)可以參考jsr規(guī)范的public view :http://today.java.net/pub/a/today/2004/04/13/JSR133.html
14年互聯(lián)網(wǎng)技術(shù)、產(chǎn)品、運(yùn)營(yíng)經(jīng)驗(yàn),前支付寶技術(shù)專家,互金創(chuàng)業(yè)公司CTO,大令保事業(yè)部總經(jīng)理。
在互金領(lǐng)域有比較強(qiáng)的產(chǎn)品以及運(yùn)營(yíng)經(jīng)驗(yàn),尤其擅長(zhǎng)用戶增長(zhǎng)、轉(zhuǎn)化、運(yùn)營(yíng)上的經(jīng)驗(yàn),兼具技術(shù)、產(chǎn)品、運(yùn)營(yíng)思維。
目前是云貓?jiān)鲩L(zhǎng)實(shí)驗(yàn)室 創(chuàng)始人
團(tuán)隊(duì)成員來(lái)自阿里等國(guó)內(nèi)知名互聯(lián)網(wǎng)公司,曾在互聯(lián)網(wǎng)金融、互聯(lián)網(wǎng)保險(xiǎn)、企業(yè)級(jí)SaaS等項(xiàng)目中負(fù)責(zé)用戶增長(zhǎng),團(tuán)隊(duì)管理的工作,擁有豐富的一線流量增長(zhǎng)經(jīng)驗(yàn)與實(shí)操手段。
歡迎關(guān)注我們,用技術(shù)驅(qū)動(dòng)增長(zhǎng)
浙公網(wǎng)安備 33010602011771號(hào)