<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    風(fēng)人園

    弱水三千,只取一瓢,便能解渴;佛法無邊,奉行一法,便能得益。
    隨筆 - 99, 文章 - 181, 評論 - 56, 引用 - 0
    數(shù)據(jù)加載中……

    多線程編程的設(shè)計模式 臨界區(qū)模式

    多線程編程的設(shè)計模式 臨界區(qū)模式(一)

    臨界區(qū)模式 Critical Section Pattern 是指在一個共享范圍中只讓一個線程執(zhí)行的模式.
    它是所有其它多線程設(shè)計模式的基礎(chǔ),所以我首先來介紹它.
    把著眼點放在范圍上,這個模式叫臨界區(qū)模式,如果把作眼點放在執(zhí)行的線程上,這個模式就叫
    單線程執(zhí)行模式.

    首先我們來玩一個鉆山洞的游戲,我 Axman,朋友 Sager,同事 Pentium4.三個人在八角游樂場
    循環(huán)鉆山洞(KAO,減肥訓(xùn)練啊),每個人手里有一個牌子,每鉆一次洞口的老頭會把當(dāng)前的次序,
    姓名,牌號顯示出來,并檢查名字與牌號是否一致.

    OK,這個游戲的參與者有游樂場老頭Geezer,Player,就是我們,還有山洞 corrie.

    public class Geezer {
    ??? public static void main(String[] args){
    ???????
    ??????? System.out.println("預(yù)備,開始!");
    ??????? Corrie c = new Corrie();//只有一個山洞,所以生存一個實例后傳給多個Player.
    ??????? new Player("Axman","001",c).start();
    ??????? new Player("Sager","002",c).start();
    ??????? new Player("Pentium4","003",c).start();
    ??? }
    }

    這個類暫時沒有什么多說的,它是一個Main的角色.

    public class Player extends Thread{
    ??? private final String name;
    ??? private final String number;
    ??? private final Corrie corrie;
    ??? public Player(String name,String number,Corrie corrie) {
    ??????? this.name = name;
    ??????? this.number = number;
    ??????? this.corrie = corrie;
    ??? }
    ???
    ??? public void run(){
    ??????? while(true){
    ??????????? this.corrie.into(this.name,this.number);
    ??????? }
    ??? }
    }
    在這里,我們把成員字段都設(shè)成final的,為了說明一個Player一旦構(gòu)造,他的名字和牌號就不能改
    變,簡單說在游戲中,我,Sager,Pentium4三個人不會自己偷偷把自己的牌號換了,也不會偷偷地去
    鉆別的山洞,如果這個游戲一旦發(fā)生錯誤,那么錯誤不在我們玩家.

    import java.util.*;
    public class Corrie {
    ??? private int count = 0;
    ??? private String name;
    ??? private String number;
    ??? private HashMap lib = new HashMap();//保存姓名與牌號的庫
    ???
    ??? public Corrie(){
    ???????
    ??????? lib.put("Axman","001");
    ??????? lib.put("Sager","002");
    ??????? lib.put("Pentium4","003");
    ?
    ??? }
    ???
    ??? public void into(String name,String number){
    ??????? this.count ++;
    ??????? this.name = name;
    ??????? this.number = number;
    ??????? if(this.lib.get(name).equals(number))
    ?test():
    ??? }
    ???
    ??? public String display(){
    ??????? return this.count+": " + this.name + "(" + this.number + ")";
    ??? }

    ??? private void test(){
    ??????? if(this.lib.get(name).equals(number))
    ??????????? ;
    ??????????? //System.out.println("OK:" + display());
    ??????? else
    ??????????? System.out.println("ERR:" + display());
    ??? }
    }
    這個類中增加了一個lib的HashMap,相當(dāng)于一個玩家姓名與牌號的庫,因為明知道Corrie只有一個實例,
    所以我用了成員對象而不是靜態(tài)實例,只是為了能在構(gòu)造方法中初始化庫中的內(nèi)容,從真正意義中說應(yīng)
    該在一個輔助類中實現(xiàn)這樣的數(shù)據(jù)結(jié)構(gòu)封裝的功能.如果不提供這個lib,那么在check的時候就要用
    if(name.equasl("Axman")){
    ?if(!number.equals("001")) //出錯
    }
    else if .......
    這樣復(fù)雜的語句,如果player大多可能會寫到手抽筋,所以用一個lib來chcek就非常容象.


    運行這個程序需要有一些耐心,因為即使你的程序?qū)懙迷俨钤诤芏鄦尉€程測試環(huán)境下也能可是正確的.
    而且多線程程序在不同的機器上表現(xiàn)不同,要發(fā)現(xiàn)這個例子的錯識,可能要運行很長一段時間,如果你的
    機器是多CPU的,那么出現(xiàn)錯誤的機會就大好多.

    在我的筆記本上最終出現(xiàn)錯誤是在11分鐘以后,出現(xiàn)的錯誤有幾鐘情況:
    1: ERR:Axman(003)
    2: ERR:Sager(002)
    第一種情況是檢查到了錯誤,我的牌號明明是001,卻打印出來003,而第二種明明沒有錯誤,卻打印了錯誤.

    事實上根據(jù)以前介紹的多線程知識,不難理解這個例子的錯誤出現(xiàn),因為into不是線程安全的,所以在其中
    一個線程執(zhí)行this.name = "Axman";后,本來應(yīng)該執(zhí)行this.numner="001",卻被切換到另一個線程中執(zhí)行
    this.number="003",然后又經(jīng)過不可預(yù)知的切換執(zhí)行其中一個的if(this.lib.get(name).equals(number))
    而出現(xiàn)1的錯誤,而在打印這個錯誤時因為display也不是線程安全的,正要打印一個錯誤的結(jié)果時,由于
    this.name或this.number其中一個字段被修改卻成了正確的匹配而出現(xiàn)錯誤2.

    另外還有可能會出現(xiàn)序號顛倒或不對應(yīng),但這個錯誤我們無法直觀地觀察,因為你根本不知道哪個序號"應(yīng)該"
    給哪個Player,而序號顛倒則有可能被滾動的屏幕所掩蓋.


    [正確的Critical Section模式的例子]
    我們知道出現(xiàn)這些錯誤是因為Corrie類的方法不是線程安全的,那么只要修改Corrie類為線程安全的類就行
    了.其它類則不需要修改,上面說過,如果出現(xiàn)錯誤那一定不是我們玩家的事:

    ?

    import java.util.*;
    public class Corrie {
    ??? private int count = 0;
    ??? private String name;
    ??? private String number;
    ??? private HashMap lib = new HashMap();//保存姓名與牌號的庫
    ???
    ??? public Corrie(){
    ???????
    ??????? lib.put("Axman","001");
    ??????? lib.put("Sager","002");
    ??????? lib.put("Pentium4","003");
    ?
    ??? }
    ???
    ??? public synchronized void into(String name,String number){
    ??????? this.count ++;
    ??????? this.name = name;
    ??????? this.number = number;
    ?test();
    ??? }
    ???
    ??? public synchronized String display(){
    ??????? return this.count+": " + this.name + "(" + this.number + ")";
    ??? }

    ??? private void test(){
    ??????? if(this.lib.get(name).equals(number))
    ??????????? ;
    ??????????? //System.out.println("OK:" + display());
    ??????? else
    ??????????? System.out.println("ERR:" + display());
    ??? }
    }

    運行這個例子,如果你的耐心,開著你的機器運行三天吧.雖然測試100天并不能說明第101天沒有出錯,
    at least,現(xiàn)在的正確性比原來那個沒有synchronized 保護的例子要可靠多了!

    到這里我們對Critical Section模式的例程有了直觀的了解,在詳細(xì)解說這個模式之前,請想一下,test
    方法安全嗎?為什么?

    所謂模式就是脫離特定的例子使用更一般化的,通用化的表達(dá)方式來察看,描述,總結(jié)相同的問題.現(xiàn)在
    我們來研究這個模式:

    共享資源(sharedResource)參與者:
    在臨界區(qū)模式中,一定有一個或一個以上的共享資源角色的參與.在上面這個例子中就是山洞(Corrie).

    共享資源參與者會被多個線程訪問,這個角色的訪問方法有兩種類型,一種是多個線程訪問也不會發(fā)生問
    題的方法,稱為線程安全的方法,另一種就是在多個線程同時訪問時會發(fā)生問題需要保護的方法,稱為不安
    全的方法.


    這里所說的線程安全和不安全的方法,不用多說大家都知道是指公開的方法.對上節(jié)最后我留下的問題而
    言,test方法是安全的,因為它是private的,只會被into方法調(diào)用,而into方法是同步的,簡單說test中的
    代碼一定會在同步塊中執(zhí)行,而display方法是public的,有可能被任何線程調(diào)用,所以它需要同步.

    對于線程安全的方法,不需要多說.而對于不安全的方法,只要定義為synchronized的就可以達(dá)到保護的
    目的.也就是多個線程同時執(zhí)行該段代碼時,只有一個線程有機會執(zhí)行,具體機制我們在多線程中同步對象
    鎖中已經(jīng)說明過.我們把這種只有一個線程能進(jìn)入的程序范圍,稱為[臨界區(qū)]


    盡管JDK5以后提供了很多功能更強,語義更準(zhǔn)確的并發(fā)控制的接口供程序員調(diào)用,但我還是極力推薦在大
    多數(shù)情況下(除非需要有效的控制)還是使用synchronized來保護臨界區(qū),因為synchronized塊的開始和結(jié)
    束是自動控制的,在離開同步塊時會自動釋放同步對象鎖.而使用java的lock對象時,你不得不每時每刻小
    心地在finally從句中調(diào)用lock對象的unlock方法,這比在finally從句中釋放數(shù)據(jù)庫連結(jié)更重要!

    [適用環(huán)境]

    1.單線程環(huán)境:單線程環(huán)境中肯定只有一個線程執(zhí)行,無論是否在臨界區(qū)中反正只有一個線程執(zhí)行,所以沒
    有必要用synchronized保護,當(dāng)然如果你非想用synchronized保護沒有問題,只是會引起性能的降低,但不
    會降低太大.這就象一個人在家里已經(jīng)關(guān)上了大門,還關(guān)著臥室的小門,除了會給你帶來一些不便之處,沒有
    什么太大的損失.

    2.多線程環(huán)境:如果這些多線程環(huán)境中各自完全獨立地運行,當(dāng)然沒有問題.但如果多個線程可能訪問同一
    SharedResource對象時,就需要使用臨界區(qū)模式來保護.有時管理線程的環(huán)境會提供一種SafeThread環(huán)境來
    確保線程的獨立,這種情況就不需要使用臨界區(qū)模式.

    3.SharedResource的狀態(tài)會發(fā)生改變的情況才需要使用這個模式,如果SharedResource對象一經(jīng)生成就不
    會改變,當(dāng)然不需要保護.(只讀模式)

    4.在必要的確保安全性的時候使用這個模式.比如java數(shù)據(jù)結(jié)構(gòu)類大多數(shù)都不是線程安全的.因為很多情況
    下發(fā)生多個線程共享沖突對程序本身并無大礙,比如用一個ArrayList或HashMap存放在線人數(shù),對于在線
    人數(shù)這種數(shù)據(jù)本來就不可能精確地計算,只是相對時間內(nèi)的一個概數(shù),所以多個線程訪問對產(chǎn)生沖突對其幾
    乎沒有影響.
    但是對于需要確保線程安全的時候,java仍然提供了大量的線程安全的數(shù)據(jù)結(jié)構(gòu)的封裝,由Collections類
    提供的synchronizedXXX()方法可以將傳入的數(shù)據(jù)結(jié)構(gòu)封裝為線程安全的.


    [性能因素]
    在程序設(shè)計中,大多數(shù)情況下,各種優(yōu)點無法共存,事實上如果使用一個模式能給其它方面的優(yōu)點也帶來提
    升那簡單就沒有理由不使用該模式了.對于安全性的提升往往要以犧牲性能為代價,所以臨界區(qū)模式會帶來
    一些性能方面的損失.如何權(quán)衡這它們之間的比例,要看程序運行的環(huán)境,目的等各方面的因素.

    1.獲取對象鎖的操作本身是要花時間的.一個線程在獲取同步對象鎖時,其實就是一個全局對象的自旋鎖,這
    個全局對象是要注冊到線程管理系統(tǒng)中的.這個過程本身需要一定的時間.但這個過程性能影響并不大.

    2.同步對象鎖被其它線程占用時需要等待.當(dāng)一個線程進(jìn)入同步塊時,獲取該同步對象的鎖,如果該鎖被其它
    線程擁有測當(dāng)前線程必須等待,從而降低性能,這方面性能的降低較大.

    提高性能的方法一是盡量減少共享資源的數(shù)量.二是盡量減小臨界區(qū)的范圍.雙檢鎖模式就是減小臨界區(qū)范

    圍的一種手段.



    [死鎖問題]
    臨界區(qū)模式中非常重要的一點是多線程程序的生命指數(shù).再安全的程序如果運行一定時間就結(jié)束自己的生命
    而不能繼續(xù)運行,那就根本不能達(dá)到設(shè)計的目的.除去系統(tǒng)突發(fā)因素,影響生命指數(shù)的最大原因就是死鎖.
    對于大家都熟悉的五個哲學(xué)家(好象是故意調(diào)侃哲學(xué)家)吃面條的例子,我們用最簡單的模型簡單為兩個哲學(xué)
    家.然后從中抽象出死鎖的最一般的條件:

    1.有多個共享資源被多線程共享.對于兩個吃面的哲學(xué)家而言就是刀和叉兩上以上的共享資源.

    2.對一個共享資源的占用還沒有釋放鎖又獲取另一個共享資源.占用了刀的時候又要獲取叉.

    3.對共享資源的占用順序是不固定的.如果哲學(xué)家按一定順序使用刀和叉,一個用完了思考時再讓給另一個
    用那就能很好地完成目標(biāo)而不會發(fā)生死鎖,正時因為對共享資源占用的順序是無法確定的.當(dāng)一個結(jié)程占用
    一個共享資源時,要獲取另一個線程占用的共享資源,而另一個線程釋放這個共享資源的條件是以獲取被原
    先被占用的共享資源時,才會發(fā)生死鎖.

    所以如果我們破壞上面其中之一的條件就不會發(fā)生死鎖問題,也就是在設(shè)計時要考慮不要同時發(fā)生上面的
    三程情況.

    [嵌套鎖定]
    對于同一對象的嵌套鎖定,例子如下:
    synchronized(this){//1
    ????System.out.println("outter");
    ????synchronized(this){//2
    ????????System.out.println("inner");
    ????}
    }
    這個例子能運行嗎?答案是可以很好地運行.
    一般以為線程運行到1時,獲取了當(dāng)前對象鎖,打印outter后,運行到2,又要獲取當(dāng)前對象鎖,而此時當(dāng)前對象
    鎖還沒有釋放,所以線程一直等在這兒發(fā)生死鎖.
    其實java是一種smart?language,在編譯的時候,它就會檢查對同一對象的嵌套鎖定.因為不可能發(fā)生在層同
    步塊中有多個線程進(jìn)入而其中一個線程要進(jìn)入內(nèi)層同步塊的情況,也就是外層同步塊本身就可以保證只有一
    個線程獲取同步對象的鎖,所以內(nèi)層同一對象的同步塊在編譯的時候已經(jīng)失去它的作用.

    [繼承和擴展]
    對于臨界區(qū)模式而言,即使我們已經(jīng)使用synchronized方法對共享資源進(jìn)行保護,但是子類在擴展接口時很可
    能將共享資源以不安全方式暴露出去.這是非常值得注意的問題.設(shè)計時應(yīng)該盡時將對共享資源的訪問方法加
    以保護,可以使用private和final等限制,另外在子類設(shè)計時也要充分考慮對父類共享資源的訪問.

    [高級主題:關(guān)于synchronized]

    其實在多線程編程基礎(chǔ)部分,我已經(jīng)談過synchronized相關(guān)的內(nèi)容.但臨界區(qū)模式是其它多線程編程模式的基
    礎(chǔ),所以在這里繼續(xù)深入一下談?wù)剆ynchronized相關(guān)的一些內(nèi)容.

    只要見到synchronized關(guān)鍵字,第一要想到的問題就是,synchronized在保護誰?

    在上面的例子中,synchronized保護的是Corrie對象的counter,name,number三個字段不被"交差賦值",也就是
    這三個字段同時只能被一個線程訪問.
    其次我們要考慮的問題是:這些對象都被妥善地保護了嗎?

    這是非常重要的問題.無論你花巨資打造一把高安全性鎖,把自己的家門牢牢地鎖住,可是你卻把門旁邊的窗子
    敞開著,那么你花巨資打造的鎖又要什么意義呢?所以要確保從任何一個通道訪問被保護的對象都被加鎖控制
    的,比如字段是否都private或protected的,對于protected的子類中的擴展方法是否能保護被保護對象.

    對于上面的例子因為display有可能被外面的方法單獨調(diào)用,所以它也必須是同步的.而test方法只會在into中
    調(diào)用,簡單說它只是所有通道被加了鎖的大房子中的一個小單元,所以不必?fù)?dān)心有人會從外部訪問它.

    要注意保護的范圍是三個同時需要保護的字段,如果它們被分別放在synchronized方法中保護,并不能保證它們
    本個字段同時只有一個線程訪問.

    那么我們就有一個問題,獲取誰的鎖呢?

    要保護一個對象,當(dāng)然直接獲取這個對象的鎖,我們上面的例子可以理解為要同時保護三個對象,那么其實就是
    要保護這個本個對象的容器.也就是它們所在的實例.如果不相關(guān)的三個對象要同時保護,一定要放在同時容納
    它們的容器中,否則無法同時保護它們的狀態(tài).對于上面的例子我們同樣可以理解為要保護的是Corrie的實例,
    因為這個實例是這三個字段的容器.所以我們用synchronized方法就是等同于synchronized(this){.......}
    如果這個游戲中有多個山洞,而只有一塊顯示牌,那以我們就需要保護多個實例的三個字段同時只被一個線程
    訪問,我們就需要synchronized(Corrie.class)來保證多個實例被多個線程訪問時只有一個對程能同時對三個
    字段訪問.
    所以獲取誰的鎖定也是一個很重要的問題,如果你選擇了錯誤的對象,就象你花巨資打了一把鎖卻鎖了別人的
    門.

    synchronized就是原子操作,簡單說在一個線程進(jìn)行同步塊中的代碼時不能進(jìn)入,這是很明顯的.但同時,多個
    同步方法或多個獲取同一對象的同步塊在同一時候也只能一個線程能訪問其中之一,因為控制誰能訪問的是要
    獲得那個同步對象的鎖.如:
    class C{
    ?synchronized? a(){}
    ?synchronized? b(){}
    }

    當(dāng)一個線程進(jìn)入同步方法a后那么其它線程當(dāng)然不能進(jìn)入a,同時也不能進(jìn)入b,因為能進(jìn)入的條件是獲取this對
    象的鎖.一個結(jié)程進(jìn)入a后this對象的鎖被這個線程獲取,其它線程進(jìn)入b也同樣要獲取這個鎖,而不僅僅是進(jìn)入
    a要獲取這個鎖.這一點一定要理解.

    理解上面的知識我們再回過頭來看原子操作.

    JLS規(guī)定對于基本類型(除long和double)以外的賦值和引用都是原子操作,并且對于引用類型的賦值和引用也是
    原子操作.

    注意這里有兩個方面的知識點:

    1.對于long和double的操作非原子性的.需要說明這只是JLS的規(guī)定,但大多數(shù)JVM的實現(xiàn)其實已經(jīng)保證了long和
    double的賦值和引用也是原子性的,只是允許某種實現(xiàn)可以不是原子性的操作.

    對于其它基本類型如int,如果一個線程執(zhí)行x = 1;另一個線程執(zhí)行x = 2;
    由于可見性的問題(多線程編程系統(tǒng)中已經(jīng)介紹),x要么就是1,要么就是2,看誰先同步到主存儲區(qū).

    但對于long,l = 1;l = 2;分別由兩個線程執(zhí)行的結(jié)果有可能不是你想象的,它們有可能是0,或1,或2,或一個其
    它的隨機數(shù),簡單說兩上線程中l(wèi)的值的部分bit位可能被另一個線程改寫.所以最可靠的是放在synchronized中
    或用volatile 保護.當(dāng)然這里說的是"有非常可靠的需要",一般而言現(xiàn)在的JVM已經(jīng)能保證long和double也是原
    子操作的.

    2.我們看到,對于引用對象的賦值和引用也是原子的.

    我們還是看javaworld上dcl的例子.

    ?那個錯誤的例子誤了好多人,(JAVA與模式的作者就是受害人),我們先不說JAVA內(nèi)存模型的原因(前面我已經(jīng)從
    JAVA內(nèi)存模型上說明了那個例子是錯誤的,我是說對那個例子的分析是錯誤的).單從對于"引用對象的賦值和引
    用也是原子的"這句話,就知道對于引用字段的賦值,絕對不可能出現(xiàn)先分配空間,然后再還沒有被始化或還沒有
    調(diào)構(gòu)造方法之前又被別的線程引用.因為當(dāng)一個線程在執(zhí)行賦值的時候是原子性的操作,其它線程的引用操作也是原子性的操作?的,在賦值操作沒有完成之前其它線程根本不可能見到"分配了空間卻沒有
    初始化或沒有調(diào)用構(gòu)造方法"的這個對象.

    不知道什么原因,這樣的一個例子從它誕生開始竟然是所有人都相信了,也許有人責(zé)疑過但我不知道.如果你有足
    夠的基礎(chǔ)知識,就不必跟著別人的感覺走!

    因為這是一個最最基礎(chǔ)的模式,暫時不介紹它與其它模式的關(guān)系.在以后介紹其它模式時反過來再和它進(jìn)行比較.

    而一些復(fù)雜的模式都是在這個簡單的模式的基礎(chǔ)上延伸的.

    posted on 2006-12-16 14:18 風(fēng)人園 閱讀(500) 評論(0)  編輯  收藏 所屬分類: Java

    主站蜘蛛池模板: 亚洲中文无码卡通动漫野外 | 免费无码成人AV在线播放不卡 | 亚洲最新中文字幕| 成人免费男女视频网站慢动作| 亚洲日本中文字幕天天更新| 久久久久亚洲AV无码专区桃色| 久久午夜无码免费| 亚洲精品av无码喷奶水糖心| 亚洲色WWW成人永久网址| 免费能直接在线观看黄的视频 | 在线观看国产一区亚洲bd| 国产精品高清视亚洲精品| 国产中文字幕免费观看| 精品在线免费观看| 亚洲成a人片在线观看天堂无码| 中文国产成人精品久久亚洲精品AⅤ无码精品 | 亚洲精品国产精品国自产网站| 四虎1515hm免费国产| 精品一区二区三区免费毛片爱| 国产精品亚洲专区无码牛牛| 亚洲狠狠久久综合一区77777| 午夜一级免费视频| 久9这里精品免费视频| 亚洲美女高清一区二区三区| 中文字幕在线观看免费视频| 免费无码婬片aaa直播表情| 亚洲成a人片毛片在线| 亚洲一级Av无码毛片久久精品| 国产成人精品免费午夜app| 亚洲精品视频免费观看| 亚洲综合色一区二区三区| 亚洲av无码不卡| 在线看片免费人成视久网| 手机永久免费的AV在线电影网| 免费在线看片网站| 国内精品乱码卡1卡2卡3免费| 亚洲综合色婷婷在线观看| 久久久久亚洲av无码专区| 夜夜春亚洲嫩草影院| 波多野结衣中文一区二区免费| 性xxxxx免费视频播放|