小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

Java內(nèi)存模型和并發(fā)機(jī)制

 seman 2009-10-26
正文

這里講的是關(guān)于Java并發(fā)機(jī)制的基礎(chǔ)模塊及如何設(shè)計(jì)合理的并發(fā)機(jī)制的抽象思維和設(shè)計(jì)模式。

 
有這么幾個(gè)知識點(diǎn):

1          “先行發(fā)生”的次序(happens-before ordering

2            volatile”修飾符的使用

3                    線程安全的延遲初始化

4                    Final”字段

5                    關(guān)于Java并發(fā)機(jī)制的一些建議

 

happens-before ordering

 

當(dāng)我們在Java里談起互斥鎖定mutualexclusion lock)時(shí),通常都指當(dāng)我首先進(jìn)入了一個(gè)互斥鎖定,其他人試圖獲得這個(gè)同樣的互斥鎖定時(shí),必須在我釋放了之后才可以。這是JavaC++里關(guān)于互斥鎖定的最重要的屬性。但事實(shí)上,這不是互斥鎖定唯一的屬性。還有一個(gè)屬性是可見性(visibility),它和次序?qū)傩裕?/span>ordering)緊密相關(guān)。

 

當(dāng)一個(gè)線程使用一個(gè)鎖定時(shí),它決定了其他線程何時(shí)可以看到該線程在鎖定后所做的更新。當(dāng)一個(gè)線程對一個(gè)變量進(jìn)行寫的操作時(shí),這個(gè)寫的操作是否會被其他線程看到將取決于該線程使用的是何種鎖定。

 

下面是一個(gè)小測驗(yàn)。有下面這段程序:

x=y=0;

//now start threads

//thread 1

x=1;

j=y;

//thread 2

y=1;

i=x;

問題是,在線程12執(zhí)行完畢后,有沒有可能ij都等于0?

 

我們知道,如果ij結(jié)果都為0的話,對y的讀(在jy里用到)一定比對y的寫先發(fā)生,類似地,對x的讀一定比對x的寫先發(fā)生?那么,這可能嗎?

 

答案是肯定的。事實(shí)上,編譯器和處理器都可能對上述程序重新排序,尤其在使用多個(gè)處理器,賦值并沒有在主內(nèi)存里同步時(shí)?,F(xiàn)代的java內(nèi)存模型使上述現(xiàn)象成為可能。上面的程序顯然是錯(cuò)誤的未經(jīng)同步的代碼,因?yàn)樗鼪]有使用鎖定。當(dāng)不同的線程需要讀寫同一個(gè)數(shù)據(jù)時(shí),必須使用鎖定的技術(shù)。

 

再看看下面一段非常關(guān)鍵的代碼??梢哉f,這段代碼是全篇演講的核心。

thread 1:

ref1.x = 1;

lock M;

glo = ref1;

unlock M;

 

thread 2:

lock M;

ref2 = glo;

unlock M;

j = ref2.x;

 

thread1里有幾個(gè)寫的操作,在對glo變量進(jìn)行寫的操作之前,它首先對對象M進(jìn)行了鎖定。在thread2里,當(dāng)thread1釋放了對M鎖定之后,它過得了對M鎖定,并開始對glo的讀操作。問題是,在thread1里的寫操作,thread2進(jìn)行讀操作時(shí),可以看到嗎?

 

答案是肯定的,原因是thread1里對M對象的釋放和thread2里對同一個(gè)對象M的獲得,形成了一個(gè)配對??梢赃@樣想,當(dāng)Mthread1里被釋放后,在thread1里所作的更新就被推出(到主內(nèi)存),隨后的在thread2里對M的獲得,就會抓取所有在thread1里所作的更新。作為thread2能得到在thread1里的更新,這就是happensbefore的次序。

 

一個(gè)釋放的操作和相匹配的之后發(fā)生的獲得操作就會建立起業(yè)已發(fā)生的次序。在同一個(gè)線程里的執(zhí)行次序也會建立起業(yè)已發(fā)生的次序(后有例子會涉及到在同一線程里的執(zhí)行次序問題)。 業(yè)已發(fā)生的次序是可以轉(zhuǎn)換的。

 

如果同時(shí)有兩筆對同一個(gè)內(nèi)存地址的訪問,其中一筆是寫的操作,并且內(nèi)存地址不是volatile的,那么這兩筆訪問在VM里的執(zhí)行次序就會按照“先行發(fā)生”的規(guī)則來排。

 

下面舉一些例子來說明問題。請看下面的程序:

int z = o.field1;

//block until obtain lock

synchronized(o){

            //get main memory value of field1 and field2

            int x = o.field1;

            int y = o.field2;

            o.field3 = x+y;

            //commit value of field3 to main memory

}

//release lock

moreCode();

 

像你從這個(gè)程序的注釋里讀到的一樣,你會期望看到,在鎖定發(fā)生后,xy會被從主要內(nèi)存里讀到的field1field2賦值,field3被賦值后在鎖定釋放后被推到主內(nèi)存里,這樣,其他線程應(yīng)該由此得到最近的更新。

 

想起來是蠻符合邏輯的。實(shí)際所發(fā)生的可能不一定如此,下面一些特殊情況會造成happensbefore的次序失效。

1 如果o是本地線程的對象?因?yàn)?a class="bodytag" target="_blank" href="http://www./articles/tag/%E9%94%81%E5%AE%9A">鎖定的是本地線程里的對象,在其他線程里不可能獲得一個(gè)相匹配的鎖定,所以對本地線程對象的鎖定不起作用,

2 是否有現(xiàn)有對o鎖定還未被釋放?如果此前已有一個(gè)對象的鎖定,在該鎖定被釋放之前,對同一個(gè)對象的再鎖定不起作用。

 

Volatile修飾符

 

當(dāng)一個(gè)字段被多個(gè)線程同時(shí)訪問,至少其中一個(gè)訪問是進(jìn)行寫操作,我們可以采用的手段有以下兩種:

1 采用鎖定來避免同時(shí)訪問

2 volatile來定義該字段,這樣做有兩個(gè)作用,一是增強(qiáng)程序的可讀性,讓讀者知道這是一個(gè)將被多線程訪問操作的字段;另外一個(gè)作用是在JVM對該字段的處理上,可以得到特殊的保證。

 

volatilejava里除鎖定之外的重要同步手段。首先,volatile字段的讀和寫都直接進(jìn)主內(nèi)存,而不會緩存在寄存器中;其次,volatile字段的讀和寫的次序是不能更改的;最后,字段的讀和寫實(shí)質(zhì)上變成了鎖定模型里的獲得和釋放。

 

對一個(gè)volatile字段的寫總是要happensbefore對它的讀;對它的寫類似于對鎖定的釋放;對它的讀類似于進(jìn)入一個(gè)鎖定。

 

volatile修飾符對可見性的影響,讓我們看看下面的代碼:

 

class Animator implements Runnable {

            private volatile boolean stop = false;

            public void stop () { stop = true;}

            public void run() {

                        while (!stop){

                                    oneStep();

                                    try { Thread.sleep(100);} …;

                      }

            }

            private void oneStep() { /*…*/ }

 

}

 

這段程序里主要有兩個(gè)線程,一個(gè)是stop,一個(gè)是run。注意,如果不用volatile來修飾stop變量,happensbefore的次序就不會得到體現(xiàn),stop線程里對stop變量的寫操作不會影響其他線程,所以編譯器不會去主內(nèi)存里讀取stop線程對stop變量的改變。這樣,在run線程里就會出現(xiàn)死循環(huán),因?yàn)樵?/span>run線程里從始至終使用的只是stop變量初始化時(shí)的值。

 

由于編譯器優(yōu)化的考慮,如果沒有volatile來修飾stop變量,run線程永遠(yuǎn)都不會讀到其他線程對stop變量的改變。

 

volatile對執(zhí)行次序保證的作用,我們看看下面的代碼:

 

class Future {

            private volatile boolean ready;

            private Object data;

            public Object get() {

                        if (!ready)

                                    return null;

                        return data;

            }

            public synchronized  void setOnce(Object o){

                        if (ready) throw…;

                        data = o;

                        ready = true;

            }

}

 

首先一點(diǎn)還是由于volatile的使用使得happensbefore的次序得以體現(xiàn),setOnce方法對ready變量的寫操作的結(jié)果一定會被get方法中的讀操作得到。

 

其次,更重要的,如果ready變量不被volatile來修飾,當(dāng)線程A叫到setOnce方法時(shí),可能按照data=o; ready=true;的次序來執(zhí)行程序,但是另一個(gè)線程B叫到setOnce方法時(shí),可能會按照ready=true;data=o;的次序來執(zhí)行??赡馨l(fā)生的一個(gè)情況是當(dāng)線程B執(zhí)行完ready=true時(shí),線程A正在檢查ready變量,結(jié)果造成data未有寫操作的情況下就完成了方法。data可能是垃圾值,舊值,或空值。

 

有關(guān)volatile的另外一點(diǎn)是被volatile修飾的變量的非原子操作化。比如,執(zhí)行volatile value++;的命令時(shí),如果在對value1后要寫回value時(shí),另外一個(gè)線程對value做寫的操作,之前加和的操作就會被影響到。

 

JVM而言,對volatile變量的讀操作是沒有額外成本的,寫操作會有一些。

 

 

線程安全的延遲初始化

 

首先有下面一段代碼:

 

Helper helper;

 

Helper getHelper() {

            if (helper == null)

                        synchronized(this){

                                    if (helper ==null)

                                                helper = new Helper();

                      }

            return helper;

}

 

這段代碼是典型的延遲初始化的產(chǎn)物。它有兩個(gè)目的:一是讓初始化的結(jié)果能被多線程共用;一是一旦對象初始化完畢,為了提高程序的效率,就不再使用同步鎖定。如果不是由于第二點(diǎn),實(shí)施對整個(gè)方法的同步其實(shí)是最保險(xiǎn)的,而不是如本段代碼中的只是對段的同步。

 

這段代碼的問題是,對helper的寫操作鎖定是存在的,但是卻沒有相匹配的獲得鎖定來讀helper,因此,happens-before的關(guān)系沒有建立起來,進(jìn)入同步段來初始化helper的唯一可能是helper==null。如果一個(gè)線程過來檢查是否helper == null,如果碰巧不是的話,它卻不能得到其他線程對helper的更新(因?yàn)闆]有happens-before的關(guān)系),所以最后它返回的很可能是一個(gè)垃圾值。

 

在這里建立happensbefore的關(guān)系的方法很簡單,就是對helper加上volatile的修飾符,volatile Helper helper;

 

線程安全的immutable對象

 

基本原則是盡可能的使用immutable對象,這樣做會有很多優(yōu)點(diǎn),包括減少對同步機(jī)制的需要;

在類里,可以將所有變量定義為final,并且在構(gòu)建完成前,不要讓其他線程看到正在構(gòu)建的對象。

 

舉個(gè)例子,線程1新建了一個(gè)類的實(shí)例;線程1在沒有使用同步機(jī)制的情況下,將這個(gè)類的實(shí)例傳遞給線程2;線程2訪問這個(gè)實(shí)例對象。在這個(gè)過程中,線程2可能在線程1對實(shí)例構(gòu)建完畢之前就得到對實(shí)例的訪問權(quán),造成了在同步機(jī)制缺失的情況下的數(shù)據(jù)競爭。

 

關(guān)于Java并發(fā)機(jī)制的一些有益建議

 

盡可能的使用已經(jīng)定義在java.util.concurrent里的類來解決問題,不要做得太底層。增強(qiáng)對java內(nèi)存模型的理解,搞懂在特定環(huán)境下釋放和獲得鎖定的意義,在你需要自己去構(gòu)想和實(shí)施并發(fā)機(jī)制時(shí),這些都會用得上。

 

在一個(gè)單線程的環(huán)境下使用并發(fā)類可能會產(chǎn)生可觀的開銷,比如對Vector每一次訪問的同步,每一筆IO操作等等。在單線程環(huán)境下,可以用ArrayList來代替Vector。也可以用bulk I/O java.nio來加快IO操作。

 

看看下面一段代碼:

 

ConcurrentHashMap<String,ID> h;

ID getID(String name){

            ID x = h.get(name);

            if (x==null){

                        x=new ID();

                        h.put(name,x);

            }         

           return x;

}

 

如果你只調(diào)用get(),或只調(diào)用put()時(shí),ConcurrentHashMap確實(shí)是線程安全的。但是,在你調(diào)用完get后,調(diào)用put之前,如果有另外一個(gè)線程調(diào)用了h.put(name,x),你再執(zhí)行h.put(name,x),就很可能把前面的操作覆蓋掉了。所以,即使在線程安全的情況下,你還有有可能違法原子操作的規(guī)則。

 

減少同步機(jī)制的開銷:

1 避免在多線程間共用可變對象

2 避免使用舊的,線程不安全的數(shù)據(jù)結(jié)構(gòu),如VectorHashtable

3 使用bulk IOjava.nio里的類

 

在使用鎖定時(shí),減少鎖定的范圍和持續(xù)時(shí)間。

    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(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ā)表

    請遵守用戶 評論公約

    類似文章 更多