最近在看《深入理解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)容,覺得受益匪淺。廢話不說,簡要的介紹一下java內(nèi)存規(guī)范。 什么是內(nèi)存規(guī)范在jsr-133中是這么定義的 A memory model describes, given a program and an execution trace of that program, whether 也就是說一個(gè)內(nèi)存模型描述了一個(gè)給定的程序和和它的執(zhí)行路徑是否一個(gè)合法的執(zhí)行路徑。對于java序言來說,內(nèi)存模型通過考察在程序執(zhí)行路徑中每一個(gè)讀操作,根據(jù)特定的規(guī)則,檢查寫操作對應(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)致目前處理器的性能越來越強(qiáng)大。目前市場上基本上都是多核處理器。如何利用多核處理器執(zhí)行程序的優(yōu)勢,使得程序性能得到極大的提升,是目前來說最重要的。 目前所有的運(yùn)算都是處理器來執(zhí)行的,我們在大學(xué)的時(shí)候就學(xué)習(xí)過一個(gè)基本概念 程序 = 數(shù)據(jù) + 算法 ,那么處理器負(fù)責(zé)計(jì)算,數(shù)據(jù)從哪里獲取了? 數(shù)據(jù)可以存放在處理器寄存器里面(目前x86處理都是基于寄存器架構(gòu)的),處理器緩存里面,內(nèi)存,磁盤,光驅(qū)等。處理器訪問這些數(shù)據(jù)的速度從快到慢依次為:寄存器,處理器緩存,內(nèi)存,磁盤,光驅(qū)。為了加快程序運(yùn)行速度,數(shù)據(jù)離處理器越近越好。但是寄存器,處理器緩存都是處理器私有數(shù)據(jù),只有內(nèi)存,磁盤,光驅(qū)才是才是所有處理器都可以訪問的全局?jǐn)?shù)據(jù)(磁盤和光驅(qū)我們這里不討論,只討論內(nèi)存)如果程序是多線程的,那么不同的線程可能分配到不同的處理器來執(zhí)行,這些處理器需要把數(shù)據(jù)從主內(nèi)存加載到處理器緩存和寄存器里面才可以執(zhí)行(這個(gè)大學(xué)操作系統(tǒng)概念里面有介紹),數(shù)據(jù)執(zhí)行完成之后,在把執(zhí)行結(jié)果同步到主內(nèi)存。如果這些數(shù)據(jù)是所有線程共享的,那么就會(huì)發(fā)生同步問題。處理器需要解決何時(shí)同步主內(nèi)存數(shù)據(jù),以及處理執(zhí)行結(jié)果何時(shí)同步到主內(nèi)存,因?yàn)橥粋€(gè)處理器可能會(huì)先把數(shù)據(jù)放在處理器緩存里面,以便程序后續(xù)繼續(xù)對數(shù)據(jù)進(jìn)行操作。所以對于內(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,寫入a的值為2,然后加載b,寫入b的值為1,同時(shí) 處理2先加載b,執(zhí)行b+1,那么b在處理器2的結(jié)果可能是1 可能是3。因?yàn)樵趌oad b之前,不知道處理器1是否已經(jīng)吧b寫會(huì)到主內(nèi)存。對于a來說,假設(shè)處理器1后于處理器2把a(bǔ)寫會(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í)行的順序是一致的。也就是說 如果兩個(gè)線程,一個(gè)線程T1對共享變量A進(jìn)行寫操作,另外一個(gè)線程T2對A進(jìn)行讀操作。如果線程T1在時(shí)間上先于T2執(zhí)行,那么T2就可以看見T1修改之后的值。 這個(gè)內(nèi)存模型比較簡單,也比較直觀,比較符合現(xiàn)實(shí)世界的邏輯。但是這個(gè)模型定義比較嚴(yán)格,在多處理器并發(fā)執(zhí)行程序的時(shí)候,會(huì)嚴(yán)重的影響程序的性能。因?yàn)槊看螌蚕碜兞康男薷亩家⒖掏綍?huì)主內(nèi)存,不能把變量保存到處理器寄存器里面或者處理器緩存里面。導(dǎo)致頻繁的讀寫內(nèi)存影響性能。
Happens-Before Memory Model : 先行發(fā)生模型。這個(gè)模型理解起來就比較困難。先介紹一個(gè)現(xiàn)行發(fā)生關(guān)系 (Happens-Before Relationship) 如果有兩個(gè)操作A和B存在A Happens-Before B,那么操作A對變量的修改對操作B來說是可見的。這個(gè)現(xiàn)行并不是代碼執(zhí)行時(shí)間上的先后關(guān)系,而是保證執(zhí)行結(jié)果是順序的。看下面例子來說明現(xiàn)行發(fā)生 A,B為共享變量,r1,r2為局部變量 初始 A=B=0 Thread1 | Thread2 1: r2=A | 3: r1=B 2: B=2 | 4: A=2 憑借直觀感覺,線程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器或者多處理器可能對指令進(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ī)則
解釋一下以上幾個(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對線程2里面就是可見的,所以r2 = 1 在分析以下,看這個(gè)例子,由于unlock操作先于lock操作,所以線程x=1對于線程2不一定是可見(不一定是現(xiàn)行發(fā)生的),所以r2的值不一定是1,有可能是x賦值為1之前的那個(gè)狀態(tài)值(假設(shè)x初始值為0,那么此時(shí)r2的值可能為0) 對于規(guī)則3,我們可以稍微修改一下我們說明的第一個(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)閷τ趘olatile類型的變量B,線程1對B的更新馬上線程2就是可見的,所以r1的值就是確定的。由于A是非valotile類型的,所以值不確定。 規(guī)則4,5,6這里就不解釋了,知道規(guī)則就可以了。 可以從以上的看出,先行發(fā)生的規(guī)則有很大的靈活性,編譯器可以對指令進(jìn)行重新排序,以便滿足處理器性能的需要。只要重新排序之后的結(jié)果,在單一線程里面執(zhí)行結(jié)果是可見的(也就是在同一個(gè)線程里面滿足先行發(fā)生原則1就可以了)。 java內(nèi)存模型是建立在先行發(fā)生的內(nèi)存模型之上的,并且再此基礎(chǔ)上,增強(qiáng)了一些。因?yàn)楝F(xiàn)行發(fā)生是一個(gè)弱約束的內(nèi)存模型,在多線程競爭訪問共享數(shù)據(jù)的時(shí)候,會(huì)導(dǎo)致不可預(yù)期的結(jié)果。有一些是java內(nèi)存模型可以接受的,有一些是java內(nèi)存模型不可以接受的。具體細(xì)節(jié)這里面就不詳細(xì)說明了。這里只說明關(guān)于java新的內(nèi)存模型重要點(diǎn)。 final字段的語義在java里面,如果一個(gè)類定義了一個(gè)final屬性,那么這個(gè)屬性在初始化之后就不可以在改變。一般認(rèn)為final字段是不變的。在java內(nèi)存模型里面,對final有一個(gè)特殊的處理。如果一個(gè)類C定義了一個(gè)非static的final屬性A,以及非static final屬性B,在C的構(gòu)造器里面對A,B進(jìn)行初始化,如果一個(gè)線程T1創(chuàng)建了類C的一個(gè)對象co,同一時(shí)刻線程T2訪問co對象的A和B屬性,如果t2獲取到已經(jīng)構(gòu)造完成的co對象,那么屬性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對象 co public static void create(){ if(co == null){ co = new FinalVarClass(); } } //線程2訪問co對象的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ā)生這種情況,原因可能是處理器對創(chuàng)建對象的指令進(jìn)行重新排序。正常情況下,對象創(chuàng)建語句co = new FinalVarClass()并不是原子的,簡單來說,可以分為幾個(gè)步驟,1 分配內(nèi)存空間 2 創(chuàng)建空的對象 3 初始化空的對象 4 把初始化完成的對象引用指向 co ,由于這幾個(gè)步驟處理器可能并發(fā)執(zhí)行,比如3,4 并發(fā)執(zhí)行,所以在create操作完成之后,co不一定馬上初始化完成,所以在vistor方法的時(shí)候,b的值可能還未初始化。但是如果是final字段,必須保證在對應(yīng)返回引用之前初始化完成。 volatile語義對于volatile字段,在現(xiàn)行發(fā)生規(guī)則里面已經(jīng)介紹過,對volatile變量的寫操作先于對變量的讀操作。也就是說任何對volatile變量的修改,都可以在其他線程里面反應(yīng)出來。對于volatile變量的介紹可以參考 本人寫的一篇文章 《java中volatile關(guān)鍵字的含義》 里面有詳細(xì)的介紹。 volatile在java新的內(nèi)存規(guī)范里面還加強(qiáng)了新的語義。在老的內(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方法,對x和v進(jìn)行寫操作,線程reader判斷,如果v=true,則打印x。在老的內(nèi)存規(guī)范里面,可能對v和x賦值順序發(fā)生改變,導(dǎo)致v的寫操作先行于x的寫操作執(zhí)行,同時(shí)另外一個(gè)線程判斷v的結(jié)果,由于v的寫操作先行于v的讀操作,所以if(v==true)返回真,于是程序執(zhí)行打印x,此時(shí)x不一定先行與System.out.println指令之前。所以顯示的結(jié)果可能為0,不一定為2 但是java新的內(nèi)存模型jsr133修正了這個(gè)問題,對于volatile語義的變量,自動(dòng)進(jìn)行l(wèi)ock 和 unlock操作包圍對變量volatile的讀寫操作。那么以上語句的順序可以表示為
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寫操作5先于發(fā)生x的讀操作9
以上只是jsr規(guī)范中一些小結(jié)行的內(nèi)容,由于jsr133規(guī)范定義了很多術(shù)語以及很多推論,上述只是簡單的介紹了一些比較重要的內(nèi)容,具體細(xì)節(jié)可以參考jsr規(guī)范的public view :http://today./pub/a/today/2004/04/13/JSR133.html
|
|