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.javascript
如上一段摘自Hashtable註釋。雖然Hashtable已經不被推薦使用了,但某種狀況下仍是會被使用。咱們知道Hashtable與HashMap一個很大的區別就是是否線程安全,Hashtable相對於HashMap來講是線程安全的。但Hashtable在使用過程當中,真的是線程安全麼?java
最近在處理Wlan Framework中的一段邏輯,該部分邏輯使用了Hashtable存儲設備列表。該設備列表在本身的工做線程中分別有添加、刪除操做,並經過binder提供了查詢操做。查詢操做須要遍歷設備列表,因爲是經過binder跨進程調用的,所以獲取列表的線程與添加、刪除操做的線程並非同一個線程,從而遇到了ConcurrentModificationException。Hashtable雖然說是線程安全的,可是它僅僅是在添加、刪除等操做時是線程安全的,若是遍歷操做處理很差,一樣會拋出異常。數組
出問題的遍歷方式以下安全
Iterator it;
it = mDeviceMap.keySet().iterator();
while(it.hasNext()) {
String key = (String)it.next();
......
}
查看Hashtable源碼,keySet返回的是Collections.SynchronizedSet對象。建立該對象時新建了一個KeySet對象,該KeySet爲Hashtable的非靜態內部類。此外還傳入了Hashtable.this賦值給了SynchronizedSet的mutex,做爲同步對象。測試
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
以下爲Collections.SynchronizedSet的實現,鑑於篇幅緣由省略了部分方法及實現內容。ui
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的實例,與Hashtable中的add、remove等操方法用的是同一把鎖。此外,經過註釋可知,使用iterator遍歷時,必需要本身進行同步操做。this
Hashtable遍歷的方法雖然有不少,但均是大同小異,這裏主要介紹兩種方案。
第一種方案,經過Hashtable的源碼可知,其put、remove等方法的同步是直接做用在方法上的,等價於使用Hashtable實例做爲同步鎖,所以以下遍歷方式是線程安全的。spa
synchronized(mDeviceMap) {
Iterator it;
it = mDeviceMap.keySet().iterator();
while(it.hasNext()) {
String key = (String)it.next();
......
}
}
因爲使用迭代器遍歷拋出異常的根本緣由是expectedModCount != modCount
,所以第二種方案即是不使用迭代器,而是從新建立一個數組,數組內容便是Hashtable中values保存的實例。這樣的好處是無需本身再作同步,代碼和邏輯看起來簡潔,固然也會帶來佔用額外空間以及效率方面的代價。線程
int size = mDeviceMap.size();
Device[] devices = mDeviceMap.values().toArray(new Device[size]);
for (Device device: devices) {
Log.d(TAG, "name = " + device.mName);
......
}
上面第二種遍歷方式,在monkey測試的時候竟然仍是拋出了異常,只不過此次是Device變量空指針異常。看到這個異常的時候一臉的懵逼。Hashtable的put方法在最開始的時候明明對value判空了,key和value都不容許爲空,那這個轉換來的value數組爲何會有空的成員?指針
雖然這個問題使用ConcurrentHashMap就能夠避免,但老是要弄個明白內心纔會踏實。那就一點點分析源碼吧。
既然是報Device爲空,那就說明轉換來的Device數組中有空成員。先分析mDeviceMap.values(),該方法同上面分析的keySet方法,返回的是SynchronizedCollection實例,這個應該沒問題,那就繼續分析後面的toArray方法了。
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
經過上面能夠看出這裏的mutex即是Hashtable實例,c即是建立的Hashtable內部類ValueCollection的實例。SynchronizedCollection支持兩種toArray方法,且均進行了同步,也就是整個轉換過程當中都有作同步操做。到這有點更懵了,既然作了同步,爲啥還會有value爲空的問題,只能接着往下看。上面c.toArray(a)調用的是ValueCollection的方法,ValueCollection繼承自AbstractCollection,那就轉到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();
//注意,這裏對傳入的數組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;
}
注意到最終返回的是數組r,且在for循環中,確實有對r中內容賦值爲null的狀況,問題應該就出在這裏了。若是咱們調用toArray(T[] a)時,提供的數組a長度比實際長度大,多出的部分就會被null填充;若是數組a的長度比實際長度小,則會新建一個數組,並一一填充。
那麼最開始的空指針是怎麼出現的呢?
int size = mDeviceMap.size();
Device[] devices = mDeviceMap.values().toArray(new Device[size]);
上面兩條語句,雖然各自都進行了同步,可是這兩條語句總體並未進行同步,當獲取size以後,其餘線程此時恰好調用了remove操做,進而致使在調用toArray的時候,實際size比咱們提供的數組a的長度要小,從而致使返回的數組多出部分會被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;
}
再來看不帶參數的toArray方法。該方法比較簡單,直接根據實際的size建立數組,並進行填充。因爲該方法調用時進行了同步,所以整個轉換過程都是同步的,從而直接使用toArray()轉換是線程安全的。