Hashtable多线程遍历问题
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)不被推薦使用了,但某種情況下還是會被使用。我們知道Hashtable與HashMap一個很大的區(qū)別就是是否線程安全,Hashtable相對于HashMap來說是線程安全的。但Hashtable在使用過程中,真的是線程安全么?
最近在處理Wlan Framework中的一段邏輯,該部分邏輯使用了Hashtable存儲設(shè)備列表。該設(shè)備列表在自己的工作線程中分別有添加、刪除操作,并通過binder提供了查詢操作。查詢操作需要遍歷設(shè)備列表,由于是通過binder跨進(jìn)程調(diào)用的,因此獲取列表的線程與添加、刪除操作的線程并不是同一個線程,從而遇到了ConcurrentModificationException。Hashtable雖說是線程安全的,但是它僅僅是在添加、刪除等操作時是線程安全的,如果遍歷操作處理不好,同樣會拋出異常。
出問題的遍歷方式如下
查看Hashtable源碼,keySet返回的是Collections.SynchronizedSet對象。創(chuàng)建該對象時新建了一個KeySet對象,該KeySet為Hashtable的非靜態(tài)內(nèi)部類。此外還傳入了Hashtable.this賦值給了SynchronizedSet的mutex,作為同步對象。
如下為Collections.SynchronizedSet的實(shí)現(xiàn),鑒于篇幅原因省略了部分方法及實(shí)現(xiàn)內(nèi)容。
如上mutex即為Hashtable的實(shí)例,與Hashtable中的add、remove等操方法用的是同一把鎖。此外,通過注釋可知,使用iterator遍歷時,必須要自己進(jìn)行同步操作。
Hashtable遍歷的方法
Hashtable遍歷的方法雖然有很多,但均是大同小異,這里主要介紹兩種方案。
第一種方案,通過Hashtable的源碼可知,其put、remove等方法的同步是直接作用在方法上的,等價(jià)于使用Hashtable實(shí)例作為同步鎖,因此如下遍歷方式是線程安全的。
由于使用迭代器遍歷拋出異常的根本原因是expectedModCount != modCount,因此第二種方案便是不使用迭代器,而是重新創(chuàng)建一個數(shù)組,數(shù)組內(nèi)容即是Hashtable中values保存的實(shí)例。這樣的好處是無需自己再做同步,代碼和邏輯看起來簡潔,當(dāng)然也會帶來占用額外空間以及效率方面的代價(jià)。
兩種toArray轉(zhuǎn)換的區(qū)別
上面第二種遍歷方式,在monkey測試的時候居然還是拋出了異常,只不過這次是Device變量空指針異常。看到這個異常的時候一臉的懵逼。Hashtable的put方法在最開始的時候明明對value判空了,key和value都不允許為空,那這個轉(zhuǎn)換來的value數(shù)組為什么會有空的成員?
雖然這個問題使用ConcurrentHashMap就可以避免,但總是要弄個明白心里才會踏實(shí)。那就一點(diǎn)點(diǎn)分析源碼吧。
既然是報(bào)Device為空,那就說明轉(zhuǎn)換來的Device數(shù)組中有空成員。先分析mDeviceMap.values(),該方法同上面分析的keySet方法,返回的是SynchronizedCollection實(shí)例,這個應(yīng)該沒問題,那就繼續(xù)分析后面的toArray方法了。
通過上面可以看出這里的mutex便是Hashtable實(shí)例,c便是創(chuàng)建的Hashtable內(nèi)部類ValueCollection的實(shí)例。SynchronizedCollection支持兩種toArray方法,且均進(jìn)行了同步,也就是整個轉(zhuǎn)換過程中都有做同步操作。到這有點(diǎn)更懵了,既然做了同步,為啥還會有value為空的問題,只能接著往下看。上面c.toArray(a)調(diào)用的是ValueCollection的方法,ValueCollection繼承自AbstractCollection,那就轉(zhuǎn)到AbstractCollection的toArray(T[] a)方法。
注意到最終返回的是數(shù)組r,且在for循環(huán)中,確實(shí)有對r中內(nèi)容賦值為null的情況,問題應(yīng)該就出在這里了。如果我們調(diào)用toArray(T[] a)時,提供的數(shù)組a長度比實(shí)際長度大,多出的部分就會被null填充;如果數(shù)組a的長度比實(shí)際長度小,則會新建一個數(shù)組,并一一填充。
那么最開始的空指針是怎么出現(xiàn)的呢?
上面兩條語句,雖然各自都進(jìn)行了同步,但是這兩條語句整體并未進(jìn)行同步,當(dāng)獲取size之后,其他線程此時剛好調(diào)用了remove操作,進(jìn)而導(dǎo)致在調(diào)用toArray的時候,實(shí)際size比我們提供的數(shù)組a的長度要小,從而導(dǎo)致返回的數(shù)組多出部分會被null填充。
再來看不帶參數(shù)的toArray方法。該方法比較簡單,直接根據(jù)實(shí)際的size創(chuàng)建數(shù)組,并進(jìn)行填充。由于該方法調(diào)用時進(jìn)行了同步,因此整個轉(zhuǎn)換過程都是同步的,從而直接使用toArray()轉(zhuǎn)換是線程安全的。
總結(jié)
轉(zhuǎn)載于:https://www.cnblogs.com/zqq-blog/p/10771978.html
總結(jié)
以上是生活随笔為你收集整理的Hashtable多线程遍历问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 页面适配的小栗子 - github
- 下一篇: JS操作字符串