Hashtable多線程遍歷問(wèn)題

If a thread-safe implementation is not needed, it is recommended to use HashMap in place of code Hashtable. If a thread-safe highly-concurrent implementation is desired, then it is recommended to use ConcurrentHashMap in place of code Hashtable.

如上一段摘自Hashtable注釋眉厨。雖然Hashtable已經(jīng)不被推薦使用了座柱,但某種情況下還是會(huì)被使用。我們知道Hashtable與HashMap一個(gè)很大的區(qū)別就是是否線程安全露乏,Hashtable相對(duì)于HashMap來(lái)說(shuō)是線程安全的嚷狞。但Hashtable在使用過(guò)程中夺溢,真的是線程安全么甥捺?

最近在處理Wlan Framework中的一段邏輯,該部分邏輯使用了Hashtable存儲(chǔ)設(shè)備列表渠旁。該設(shè)備列表在自己的工作線程中分別有添加攀例、刪除操作,并通過(guò)binder提供了查詢操作顾腊。查詢操作需要遍歷設(shè)備列表粤铭,由于是通過(guò)binder跨進(jìn)程調(diào)用的,因此獲取列表的線程與添加杂靶、刪除操作的線程并不是同一個(gè)線程梆惯,從而遇到了ConcurrentModificationException。Hashtable雖說(shuō)是線程安全的伪煤,但是它僅僅是在添加加袋、刪除等操作時(shí)是線程安全的,如果遍歷操作處理不好抱既,同樣會(huì)拋出異常职烧。

出問(wèn)題的遍歷方式如下

Iterator it;
it = mDeviceMap.keySet().iterator();
while(it.hasNext()) {
    String key = (String)it.next();
    ......
}

查看Hashtable源碼,keySet返回的是Collections.SynchronizedSet對(duì)象。創(chuàng)建該對(duì)象時(shí)新建了一個(gè)KeySet對(duì)象蚀之,該KeySet為Hashtable的非靜態(tài)內(nèi)部類蝗敢。此外還傳入了Hashtable.this賦值給了SynchronizedSet的mutex,作為同步對(duì)象足删。

public Set<K> keySet() {
    if (keySet == null)
      keySet = Collections.synchronizedSet(new KeySet(), this);
    return keySet;
}

如下為Collections.SynchronizedSet的實(shí)現(xiàn)寿谴,鑒于篇幅原因省略了部分方法及實(shí)現(xiàn)內(nèi)容。

static class SynchronizedCollection<E> implements Collection<E>, Serializable {
        final Collection<E> c;  // Backing Collection
        final Object mutex;     // Object on which to synchronize
        SynchronizedCollection(Collection<E> c, Object mutex) {
            this.c = Objects.requireNonNull(c);
            this.mutex = Objects.requireNonNull(mutex);
        }
        public Object[] toArray() {
            synchronized (mutex) {return c.toArray();}
        }
        public <T> T[] toArray(T[] a) {
            synchronized (mutex) {return c.toArray(a);}
        }

        public Iterator<E> iterator() {
            return c.iterator(); // Must be manually synched by user!
        }
}

如上mutex即為Hashtable的實(shí)例失受,與Hashtable中的add讶泰、remove等操方法用的是同一把鎖。此外拂到,通過(guò)注釋可知痪署,使用iterator遍歷時(shí),必須要自己進(jìn)行同步操作兄旬。

Hashtable遍歷的方法

Hashtable遍歷的方法雖然有很多狼犯,但均是大同小異,這里主要介紹兩種方案领铐。
第一種方案悯森,通過(guò)Hashtable的源碼可知,其put绪撵、remove等方法的同步是直接作用在方法上的瓢姻,等價(jià)于使用Hashtable實(shí)例作為同步鎖,因此如下遍歷方式是線程安全的莲兢。

synchronized(mDeviceMap) {
    Iterator it;
    it = mDeviceMap.keySet().iterator();
    while(it.hasNext()) {
        String key = (String)it.next();
        ......
    }
}

由于使用迭代器遍歷拋出異常的根本原因是expectedModCount != modCount汹来,因此第二種方案便是不使用迭代器续膳,而是重新創(chuàng)建一個(gè)數(shù)組改艇,數(shù)組內(nèi)容即是Hashtable中values保存的實(shí)例。這樣的好處是無(wú)需自己再做同步坟岔,代碼和邏輯看起來(lái)簡(jiǎn)潔谒兄,當(dāng)然也會(huì)帶來(lái)占用額外空間以及效率方面的代價(jià)。

int size = mDeviceMap.size();
Device[] devices = mDeviceMap.values().toArray(new Device[size]);
for (Device device: devices) {
    Log.d(TAG, "name = " + device.mName);
    ......
}
兩種toArray轉(zhuǎn)換的區(qū)別

上面第二種遍歷方式社付,在monkey測(cè)試的時(shí)候居然還是拋出了異常承疲,只不過(guò)這次是Device變量空指針異常∨缚В看到這個(gè)異常的時(shí)候一臉的懵逼燕鸽。Hashtable的put方法在最開始的時(shí)候明明對(duì)value判空了,key和value都不允許為空啼辣,那這個(gè)轉(zhuǎn)換來(lái)的value數(shù)組為什么會(huì)有空的成員啊研?

雖然這個(gè)問(wèn)題使用ConcurrentHashMap就可以避免,但總是要弄個(gè)明白心里才會(huì)踏實(shí)。那就一點(diǎn)點(diǎn)分析源碼吧党远。

既然是報(bào)Device為空削解,那就說(shuō)明轉(zhuǎn)換來(lái)的Device數(shù)組中有空成員。先分析mDeviceMap.values()沟娱,該方法同上面分析的keySet方法氛驮,返回的是SynchronizedCollection實(shí)例,這個(gè)應(yīng)該沒(méi)問(wèn)題济似,那就繼續(xù)分析后面的toArray方法了矫废。

public Object[] toArray() {
    synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
    synchronized (mutex) {return c.toArray(a);}
}

通過(guò)上面可以看出這里的mutex便是Hashtable實(shí)例,c便是創(chuàng)建的Hashtable內(nèi)部類ValueCollection的實(shí)例砰蠢。SynchronizedCollection支持兩種toArray方法磷脯,且均進(jìn)行了同步,也就是整個(gè)轉(zhuǎn)換過(guò)程中都有做同步操作娩脾。到這有點(diǎn)更懵了赵誓,既然做了同步,為啥還會(huì)有value為空的問(wèn)題柿赊,只能接著往下看俩功。上面c.toArray(a)調(diào)用的是ValueCollection的方法,ValueCollection繼承自AbstractCollection碰声,那就轉(zhuǎn)到AbstractCollection的toArray(T[] a)方法诡蜓。

public <T> T[] toArray(T[] a) {
    // Estimate size of array; be prepared to see more or fewer elements
    int size = size();
    //注意,這里對(duì)傳入的數(shù)組length與size做了比較
    T[] r = a.length >= size ? a :
              (T[])java.lang.reflect.Array
              .newInstance(a.getClass().getComponentType(), size);
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        if (! it.hasNext()) { // fewer elements than expected
            if (a == r) {
                r[i] = null; // null-terminate
            } else if (a.length < i) {
                return Arrays.copyOf(r, i);
            } else {
                System.arraycopy(r, 0, a, 0, i);
                if (a.length > i) {
                    a[i] = null;
                }
            }
            return a;
        }
        r[i] = (T)it.next();
    }
    // more elements than expected
    return it.hasNext() ? finishToArray(r, it) : r;
}

注意到最終返回的是數(shù)組r胰挑,且在for循環(huán)中蔓罚,確實(shí)有對(duì)r中內(nèi)容賦值為null的情況,問(wèn)題應(yīng)該就出在這里了瞻颂。如果我們調(diào)用toArray(T[] a)時(shí)豺谈,提供的數(shù)組a長(zhǎng)度比實(shí)際長(zhǎng)度大,多出的部分就會(huì)被null填充贡这;如果數(shù)組a的長(zhǎng)度比實(shí)際長(zhǎng)度小茬末,則會(huì)新建一個(gè)數(shù)組,并一一填充盖矫。

那么最開始的空指針是怎么出現(xiàn)的呢丽惭?

int size = mDeviceMap.size();
Device[] devices = mDeviceMap.values().toArray(new Device[size]);

上面兩條語(yǔ)句,雖然各自都進(jìn)行了同步辈双,但是這兩條語(yǔ)句整體并未進(jìn)行同步责掏,當(dāng)獲取size之后,其他線程此時(shí)剛好調(diào)用了remove操作湃望,進(jìn)而導(dǎo)致在調(diào)用toArray的時(shí)候换衬,實(shí)際size比我們提供的數(shù)組a的長(zhǎng)度要小局义,從而導(dǎo)致返回的數(shù)組多出部分會(huì)被null填充。

public Object[] toArray() {
    // Estimate size of array; be prepared to see more or fewer elements
    Object[] r = new Object[size()];
    Iterator<E> it = iterator();
    for (int i = 0; i < r.length; i++) {
        if (! it.hasNext()) // fewer elements than expected
            return Arrays.copyOf(r, i);
        r[i] = it.next();
    }
    return it.hasNext() ? finishToArray(r, it) : r;
}

再來(lái)看不帶參數(shù)的toArray方法冗疮。該方法比較簡(jiǎn)單萄唇,直接根據(jù)實(shí)際的size創(chuàng)建數(shù)組,并進(jìn)行填充术幔。由于該方法調(diào)用時(shí)進(jìn)行了同步另萤,因此整個(gè)轉(zhuǎn)換過(guò)程都是同步的,從而直接使用toArray()轉(zhuǎn)換是線程安全的诅挑。

總結(jié)

  1. Hashtable已經(jīng)不推薦使用了四敞,如果無(wú)需考慮線程安全,直接使用Hashmap拔妥;需要考慮線程安全時(shí)忿危,使用ConcurrentHashMap。
  2. Hashtable遍歷時(shí)没龙,還是需要注意線程安全問(wèn)題铺厨。
  3. SynchronizedCollection的兩種toArray方法是不同的,如無(wú)特殊要求硬纤,建議使用無(wú)參的方法解滓。
  4. 遇到問(wèn)題要多看源碼實(shí)現(xiàn)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筝家,一起剝皮案震驚了整個(gè)濱河市洼裤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌溪王,老刑警劉巖腮鞍,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異莹菱,居然都是意外死亡移国,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門芒珠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)桥狡,“玉大人搅裙,你說(shuō)我怎么就攤上這事皱卓。” “怎么了部逮?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵娜汁,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我兄朋,道長(zhǎng)掐禁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮傅事,結(jié)果婚禮上缕允,老公的妹妹穿的比我還像新娘。我一直安慰自己蹭越,他們只是感情好障本,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著响鹃,像睡著了一般驾霜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上买置,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天粪糙,我揣著相機(jī)與錄音,去河邊找鬼忿项。 笑死蓉冈,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的轩触。 我是一名探鬼主播洒擦,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼怕膛!你這毒婦竟也來(lái)了熟嫩?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤褐捻,失蹤者是張志新(化名)和其女友劉穎掸茅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體柠逞,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昧狮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了板壮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逗鸣。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绰精,靈堂內(nèi)的尸體忽然破棺而出撒璧,到底是詐尸還是另有隱情,我是刑警寧澤笨使,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布卿樱,位于F島的核電站,受9級(jí)特大地震影響硫椰,放射性物質(zhì)發(fā)生泄漏繁调。R本人自食惡果不足惜萨蚕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹄胰。 院中可真熱鬧岳遥,春花似錦、人聲如沸裕寨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)帮坚。三九已至妻往,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間试和,已是汗流浹背讯泣。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留阅悍,地道東北人好渠。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像节视,于是被迫代替她去往敵國(guó)和親拳锚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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

  • 在一個(gè)方法內(nèi)部定義的變量都存儲(chǔ)在棧中寻行,當(dāng)這個(gè)函數(shù)運(yùn)行結(jié)束后霍掺,其對(duì)應(yīng)的棧就會(huì)被回收,此時(shí)拌蜘,在其方法體中定義的變量將不...
    Y了個(gè)J閱讀 4,417評(píng)論 1 14
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法杆烁,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法简卧,繼承相關(guān)的語(yǔ)法兔魂,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 31,631評(píng)論 18 399
  • 六一節(jié)那天举娩,學(xué)校都是五花八門析校,色彩斑斕。剛開始的時(shí)候老師讓我們看侏羅紀(jì)的電影铜涉,我從來(lái)都沒(méi)有見過(guò)恐龍智玻,真是太棒了。后...
    想念xxx閱讀 186評(píng)論 0 0
  • LinkCampus is a place that QingDao government responded t...
    SherryMiyano閱讀 394評(píng)論 1 0
  • 今年來(lái)到南京分撥中心骄噪,我陷入到人生最深的一次抑郁和難受尚困。雖然知道自己患抑郁癥很長(zhǎng)時(shí)間,但是待在這個(gè)地方真的很難受链蕊。...
    翟可闖閱讀 184評(píng)論 0 0