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é)
- Hashtable已經(jīng)不推薦使用了四敞,如果無(wú)需考慮線程安全,直接使用Hashmap拔妥;需要考慮線程安全時(shí)忿危,使用ConcurrentHashMap。
- Hashtable遍歷時(shí)没龙,還是需要注意線程安全問(wèn)題铺厨。
- SynchronizedCollection的兩種toArray方法是不同的,如無(wú)特殊要求硬纤,建議使用無(wú)參的方法解滓。
- 遇到問(wèn)題要多看源碼實(shí)現(xiàn)。