《java并發(fā)編程實戰(zhàn)》第四章:設(shè)計線程安全的類

線程安全的類

前面德章節(jié)主要介紹java一些底層基礎(chǔ)的并發(fā)實現(xiàn)機制和java的一些并發(fā)基礎(chǔ)知識瀑梗,本章節(jié)主要是用上述的這些知識來構(gòu)建線程安全的類另锋。本章將會把前面介紹的不可變對象(Immutable Object)极祸、對象的線程級限制(Thread Confinement)蛋欣、鎖等技術(shù)綜合運用來構(gòu)建符合各種要求的線程安全的類丐巫。


java監(jiān)視器模式(Java monitor pattern)
下面先看一個典型的java監(jiān)視器模式代碼:

public class PersonList{
    private final List<Person> myList = new ArrayList<Person>();

    public synchronized void addPerson(Person person){
        myList.add(person);
    }

    public synchronized boolean containsPerson(Person p){
        return myList.contains(p);
    }

    // public synchronized Person get(index i){
    //   return myList.get(i);
    // }
}

代碼中PersonList只有一個成員變量myList锨侯,其被聲明為private final形式的List類型饺谬,通過前面文章 不用鎖,也能實現(xiàn)線程安全的緩存系統(tǒng)的學(xué)習(xí)我們知道final變量的使用可以保證通過PersonList的構(gòu)造方法返回的PersonList類型的實例的成員變量已經(jīng)被初始化完成捂刺。而private的使用保證其成員變量不會被其他線程直接訪問,其addPerson和containsPerson方法都是加鎖的方法,這樣就可以保證同一時間只能有一個線程來操作成員變量myList叠萍。注意到myList是ArrayList類型的變量芝发,其不是線程安全的類,但是通過java的監(jiān)視器模式苛谷,我們實現(xiàn)了PersonList類的線程安全辅鲸。這里我們沒有,也不需要考慮Person類的線程安全性腹殿,因為PersonList類并沒有提供任何方法讓線程獲得myList容器中的Person對象独悴。但是如果PersonList添加有如上被注釋的get方法,那么就需要保證Person類也是線程安全的了锣尉。這就是java監(jiān)視器設(shè)計模式,或許你在平時也用過此模式刻炒,只不過不知道其具體的名字罷了。


從零設(shè)計線程安全的類
為了讓線程安全的類的設(shè)計更有實戰(zhàn)意義自沧,我們從一個真實的開發(fā)需求出發(fā)坟奥,具體需求如下:
某民航公司要求設(shè)計一套客機位置顯示系統(tǒng),要求能夠顯示客機在天空中的經(jīng)度和維度值拇厢;同時每架客機身上都裝有GPS定位系統(tǒng)爱谁,會實時向總控制臺返回自己的位置信息;每架飛機都擁有自己獨一無二的名字孝偎。
從需求上分析我們需要設(shè)計兩種類型的線程:display線程访敌,用來在界面上顯示飛機的位置;update線程衣盾,用來實時更新每架飛機的位置信息這兩個類型的線程由同事A來負(fù)責(zé)寺旺,我們只需要提供相應(yīng)的接口即可。我們將綜合運用前幾章講到的知識和本章的java監(jiān)視器模式來設(shè)計一個符合要求的顯示系統(tǒng)势决。

  1. java監(jiān)視器模式
    飛機在空中的位置可以用經(jīng)度和維度來表示阻塑,其名字可以用一個字符串來表示,于是我們定義好飛機類:
class MutablePlane {
    public float  x; //維度
    public float  y; //經(jīng)度
    public String name; //飛機名字

    public MutablePlane(MutablePlane plane){
        this.x = plane.x;
        this.y = plane.y;
        this.name = plane.name;
    }   
}

有了飛機的類果复,我們還需要設(shè)計一個類來存儲民航公司的所有飛機的信息的類陈莽,同時此類還要給display線程提供所有飛機信息的接口(方法),此類還要為update類提供更新指定飛機坐標(biāo)位置的接口(方法);當(dāng)然此類可以被display線程,update線程訪問据悔,要保證其線程安全传透,我們首先用java監(jiān)視器模式來設(shè)計此類耘沼,具體類設(shè)計如下:

public class MonitorSystem {
    private final Map<String,MutablePlane> planes;

    public MonitorSystem(){
        //對planes進(jìn)行賦值操作极颓,初始化planes 這里是略寫
        planes = new HashMap<String, MutablePlane>();
    }

    private Map<String,MutablePlane> deepcopy(Map<String,MutablePlane> m){
        Map<String,MutablePlane> map = new HashMap<String,MutablePlane>(); //1
        for(String name : m.keySet()){                                    //1
            map.put(name,new MutablePlane(m.get(name)));                  //1
        }                                                                 //1

        return Collections.unmodifiableMap(map);                          //2
    }

    //update線程使用,用來實時更新指定飛機的位置信息
    public synchronized void setLocation(String name, float x, float y) {
        MutablePlane mutablePlane = planes.get(name);
        if(mutablePlane == null){
            throw new RuntimeException("the plane does not exist for name: " + name);
        }

        mutablePlane.x = x;
        mutablePlane.y = y;
    }

    //display線程使用群嗤,用來在界面上顯示飛機的位置信息
    public synchronized  Map<String,MutablePlane> getPlanes(){
        return deepcopy(planes);
    }
}

MonitorSystem類的設(shè)計是一個典型的java監(jiān)視器模式菠隆,我們注意下其getPlanes方法,這個方法調(diào)用了一個deepcopy方法,deepcopy方法先通過//1處的代碼深度復(fù)制planes類骇径,然后通過Collections的unmodifiableMap方法返回一個可讀不可寫的map類躯肌。之所以這么復(fù)雜的設(shè)計getPlanes方法而不是直接返回planes成員變量給display線程是因為planes包含的對象是線程非線程安全的類,我們把planes變量直接暴露給display線程,就相當(dāng)于把線程不安全的MutablePlane對象暴露給別的線程破衔,我們不知道同事A要怎樣設(shè)計他的display線程清女,為了安全起見,我們不能讓其他線程獲得MutablePlane對象的引用晰筛,以防止其對MutablePlane對象做修改嫡丙。這樣實現(xiàn)的getPlanes方法還有一個特點:每次display線程調(diào)用getPlanes方法后得到的飛機位置信息可能已經(jīng)“過時”,在獲得飛機位置信息后读第,update線程可能又對飛機的位置信息做了更新曙博,如果不再次調(diào)用getPlanes方法,是不能獲得新的更新信息的怜瞒。

  1. 使用代理
    觀察MonitorSystem類父泳,因為我們使用的成員變量是非線程安全的HashMap類型,所以我們設(shè)計的getPlanes方和setLocations方法使用了同步吴汪。如果我們把成員變量變成線程安全的:
public class DelegatingSystem {
    private final ConcurrentHashMap<String,ImmutablePlane> planes;
    private final Map<String,ImmutablePlane> unmodifiableMap;

    public DelegatingSystem() {

        //對planes進(jìn)行賦值操作惠窄,初始化planes 這里是略寫
        this.planes = new ConcurrentHashMap<>();
        this.unmodifiableMap = Collections.unmodifiableMap(planes);
    }

    //update線程使用,用來實時更新指定飛機的位置信息
    public void setLocation(String name, float x, float y) {
        ImmutablePlane mutablePlane = planes.remove(name);
        if(mutablePlane == null){
            throw new RuntimeException("the plane does not exist for name: " + name);
        }

        planes.put(name, new ImmutablePlane(name, x, y));

    }

    //display線程使用浇坐,用來在界面上顯示飛機的位置信息
    public Map<String,ImmutablePlane> getPlanes(){
        return unmodifiableMap;
    }
}

在DelegatingSystem中我們沒有使用同步(沒有用synchronized關(guān)鍵字聲明方法),因為我們把線程安全代理給了其成員變量planes,planes的類型是ConcurrentHashMap近刘。為了保證DelegatingSystem的線程安全擒贸,我們還要保證其所存儲的飛機信息對象的安全,如果我們繼續(xù)沿用上面的MutablePlane類觉渴,那么display線程可以通過getPlanes拿到MutablePlane對象介劫,而MutablePlane對象是非線程安全的。為此我們定義了新的不可變對象ImmutablePlane如下:

public class ImmutablePlane {
    public final float x;
    public final float y;
    public final String name;

    public ImmutablePlane(String name, float x, float y) {
        this.x = x;
        this.y = y;
        this.name = name;
    }
}

這樣就可以保證DelegatingSystem的線程安全案淋,當(dāng)然我們?nèi)匀豢梢韵隡onitorSystem那么使用MutablePlane類座韵,但其getPlanes方法就需要返回的是對planes成員變量的深度拷貝,就像MonitorSystem的getPlanes方法一樣踢京。我們這里用了線程安全的ImmutablePlane類誉碴,來避免深度拷貝,這樣在飛機特別多的情況下瓣距,可以節(jié)省深度拷貝方法的調(diào)用時間黔帕,從而提高響應(yīng)效率,當(dāng)然DelegatingSystem還有另外一個特價蹈丸,就是display線程能實時獲得update線程更新飛機后的最新位置信息成黄,而不需要重新調(diào)用getPlanes方法呐芥。兩種方式各有利弊,需要具體情況下具體選擇奋岁。


利用現(xiàn)有線程安全的類
我們從DelegatingSystem類的設(shè)計中可以看到其使用了java基礎(chǔ)類庫中的ConcurrentHashMap類來保證其線程安全思瘟,這往往是最高效最簡單也是最安全的方式。

  • 簡單:它避免也我們像MonitorSystem類那樣每個方法都要自己設(shè)計同步邏輯闻伶,像setLocation和getPlanes方法用到了鎖滨攻,但deepCopy就可以不用鎖,這對開發(fā)者的并發(fā)知識要求很高蓝翰,設(shè)計起來不是很簡單铡买。
  • 高效:java的concurrent包中的各種類設(shè)計的非常精巧,在保證線程安全的同時有可以有很高的并發(fā)率霎箍,我們很難也沒有比較設(shè)計出比先有并發(fā)類更高效的線程安全的類奇钞。
  • 安全:java基礎(chǔ)庫里面的類,都是經(jīng)過千錘百煉的漂坏。我們自己設(shè)計的類往往因為測試不夠或者設(shè)計不夠縝密景埃,而導(dǎo)致意想不到的問題,而在多線程環(huán)境下線程安全的類更是難上加難顶别,需要開發(fā)者能深入理解java的并發(fā)機制谷徙,同時準(zhǔn)確把握設(shè)計需求,稍有不慎就可能留下bug驯绎,而在 《java并發(fā)編程實戰(zhàn)》第二章:線程安全 開頭我們就介紹了多線程bug的嚴(yán)重危害完慧,這里不再贅述。

當(dāng)然利用現(xiàn)有線程安全的類構(gòu)建新的線程安全的類剩失,也并不是沒有缺點和需要注意的事項屈尼,下面通過幾種常見的構(gòu)建方式也說說其缺點和需要注意的事項。假設(shè)我們有如下需求:設(shè)計一個線程安全的類拴孤,能夠?qū)崿F(xiàn)線程安全的讀寫同時還提供額外的方法put-if-absent脾歧,具體語意為:向其中添加元素如果此類中不存在此元素就添加,如果已經(jīng)存在演熟,就不添加此元素鞭执。我們用不同的方法來實現(xiàn)這個功能。

  1. 改造現(xiàn)有類
    我們發(fā)現(xiàn)有很多現(xiàn)有的線程安全的類可以滿足需求的讀寫方法的要求芒粹,我們需要做的是設(shè)計一個線程安全的put-if-absent方法兄纺,我們可以直接選擇修改一個線程安全的類,比如Vector,在其中添加一個線程安全的方法來實現(xiàn)put-if-absent的邏輯化漆。
    這種方式的優(yōu)點是可以最大限度的保持代碼的健壯性估脆,前提是你能獲得CopyOnWriteArrayList的修改權(quán)限,而且對其線程安全策略十分了解获三。
  2. 繼承現(xiàn)有類
    我們可以構(gòu)造如下的類:
public class BetterVector<E> extends Vector<E> {
    public synchronized boolean putIfAbsent(E x){
        boolean absent = !contains(x);
        if(absent){
            add(x);
        }
        return absent;
    }
}

這樣設(shè)計的BetterVector類可以實現(xiàn)上述的要求旁蔼,但是它健壯性就不如前面德直接改造現(xiàn)有類。因為我們putIfAbsent方法用的對象的內(nèi)置鎖,而且Vector類的確也是用的內(nèi)置鎖來實現(xiàn)的線程安全疙教。但是如果我們繼承的不是java基礎(chǔ)的線程安全類棺聊,我們繼承了別的線程安全的類A,使用了其線程安全策略贞谓。如果之后類A更換線程安全策略限佩,比如從使用內(nèi)置鎖變?yōu)槭褂肦eentantLock來實現(xiàn)其線程安全策略,那么我們的代碼的線程安全性就會被無聲無息的破壞裸弦。這也是繼承現(xiàn)有類來實現(xiàn)線程安全的缺點之一祟同。

最后的最后,需要強調(diào)的是不管你是從0開始設(shè)計了一個線程安全的類理疙,還是用java現(xiàn)有的線程安全的類設(shè)計出來一個類晕城,我們都要需要為我們設(shè)計的類寫好說明文檔,這樣不僅僅是利于后面的維護(hù)人員日常維護(hù)窖贤,也對我們后續(xù)開發(fā)和查閱代碼也相當(dāng)重要砖顷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赃梧,隨后出現(xiàn)的幾起案子滤蝠,更是在濱河造成了極大的恐慌,老刑警劉巖授嘀,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件物咳,死亡現(xiàn)場離奇詭異,居然都是意外死亡蹄皱,警方通過查閱死者的電腦和手機览闰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來巷折,“玉大人焕济,你說我怎么就攤上這事】福” “怎么了晴弃?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長逊拍。 經(jīng)常有香客問我上鞠,道長,這世上最難降的妖魔是什么芯丧? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任芍阎,我火速辦了婚禮,結(jié)果婚禮上缨恒,老公的妹妹穿的比我還像新娘谴咸。我一直安慰自己轮听,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布岭佳。 她就那樣靜靜地躺著血巍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪珊随。 梳的紋絲不亂的頭發(fā)上述寡,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機與錄音叶洞,去河邊找鬼鲫凶。 笑死,一個胖子當(dāng)著我的面吹牛衩辟,可吹牛的內(nèi)容都是我干的螟炫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼艺晴,長吁一口氣:“原來是場噩夢啊……” “哼不恭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起财饥,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤换吧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钥星,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沾瓦,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年谦炒,在試婚紗的時候發(fā)現(xiàn)自己被綠了贯莺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡宁改,死狀恐怖缕探,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情还蹲,我是刑警寧澤爹耗,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站谜喊,受9級特大地震影響潭兽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斗遏,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一山卦、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诵次,春花似錦账蓉、人聲如沸枚碗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽肮雨。三九已至,卻和暖如春归敬,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鄙早。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工汪茧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人限番。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓舱污,卻偏偏與公主長得像,于是被迫代替她去往敵國和親弥虐。 傳聞我的和親對象是個殘疾皇子扩灯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容