日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Java內(nèi)存模型

 feimishiwo 2014-12-18

最近在看《深入理解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
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.

也就是說一個(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ī)則

  • 1 在同一個(gè)線程里面,按照代碼執(zhí)行的順序(也就是代碼語義的順序),前一個(gè)操作先于后面一個(gè)操作發(fā)生
  • 2 對一個(gè)monitor對象的解鎖操作先于后續(xù)對同一個(gè)monitor對象的鎖操作
  • 3 對volatile字段的寫操作先于后面的對此字段的讀操作
  • 4 對線程的start操作(調(diào)用線程對象的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對線程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

 

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多