技術博客已遷移至個人頁,歡迎查看 yloopdaed.icu
您也可以關注 JPP - 這是一個Java養成計劃,需要您的加入。
前言
HashMap源碼中定義的成員變量并不多,其中我們最不熟悉的應該就是modCount,那么它到底是做什么的呢?
如果你沒時間思考這篇文章,你可以直接跳轉到 9.結論 處
modCount
modCount在HashMap中記錄的是HashMap對象被修改的次數,這里專業的說法是集合在結構上修改時被會記錄在modCount中。
文中源碼版本為 JDK1.7,modCount的部分在JDK1.8中作用是相同的。只因為JDK1.7中源碼比較簡潔,所以本文選用JDK1.7來縮減篇幅。
在源碼中記錄到的modCount++的方法包括:
HashMap put方法[圖片上傳中...(modcount.jpg-dff60f-1604249139377-0)]
HashMap的remove->removeEntryForKey方法 通過key移除元素
HashMap的removeMapping方法,通過object移除元素
HashMap的clear方法
從這里可以看出,結構上的修改主要是添加和刪除兩部分。
線程不安全
我們都知道在JDK1.7中HashMap是線程不安全的,這個 不安全 我是分兩方面理解的:
1 多線程數組擴容時出現循環鏈表問題
因為擴容時鏈表順序會反轉,所以多線程操作時可能會出現循環鏈表的情況,那么在get方法時就會死循環
JDK1.8中也修復了這個問題
2 多線程讀寫時造成數據混亂的問題
HashMap中有引入了一個 fast-fail 的概念,目的是避免高并發讀寫造成的數據錯亂的隱患。
expectedModCount
expectedModCount這個變量被記錄在HashIterator迭代器中。顧名思義,表示期望的修改次數,當期望修改的次數不等于實際修改的次數時,就會觸發 fast-fail 快速失敗的容錯處理
fast-fail
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
...
}
迭代器調用 next() 方法時會調用 nextEntry() 方法,方法中首先會判斷 modCount 與 expectedModCount 是否相等
如果不相等直接拋出 java.util.ConcurrentModificationException 異常
GeeksForGeeks中的解釋為:
In multi threaded environment, if during the detection of the resource, any method finds that there is a concurrent modification of that object which is not permissible, then this ConcurrentModificationException might be thrown.
- If this exception is detected, then the results of the iteration are undefined.
- Generally, some iterator implementations choose to throw this exception as soon as it is encountered, called fail-fast iterators.
For example: If we are trying to modify any collection in the code using a thread, but some another thread is already using that collection, then this will not be allowed.
在多線程環境中,如果在檢測資源期間,任何方法發現該對象存在并發修改,而這是不允許的,則可能會拋出此ConcurrentModificationException。
1 如果檢測到此異常,則迭代結果不確定。
2 通常,某些迭代器實現選擇將遇到此異常的異常立即拋出,稱為快速失敗迭代器。
例如:如果我們試圖使用一個線程來修改代碼中的任何集合,但是另一個線程已經在使用該集合,則將不允許這樣做。
驗證
相關代碼可以在 JPP/ConcurrentModificationExceptionDemo類中查看。
HashMap m = new HashMap();
for (int i = 0; i <100 ; i++) {
m.put(String.valueOf(i), "value"+i);
}
new Thread(new Runnable() {
@Override
public void run() {
Iterator iterator = m.keySet().iterator();
while (iterator.hasNext()) {
String next = (String) iterator.next();
if (Integer.parseInt(next) % 2 == 0) {
System.out.println("thread 1");
iterator.remove();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Iterator iterator = m.keySet().iterator();
while (iterator.hasNext()) {
String next = (String) iterator.next();
System.out.println(m.get(next));
}
}
}).start();
這里第一個線程中的 System.out.println("thread 1");
的作用是 觸發數據和內存同步。
這部分內容和寄存器的 緩存行 知識有關,如果不觸發數據和內存同步,第二個線程無法正確獲取modCount。
單線程錯誤案例
HashMap m = new HashMap();
m.put("key1", "value2");
m.put("key2", "value2");
for (String key: m.keySet()) {
if (key.equals("key2")) {
m.remove(key);
}
}
這個代碼塊也有可能發生 fast-fail
我們來看一下上面代碼塊編譯后的class文件
HashMap m = new HashMap();
m.put("key1", "value2");
m.put("key2", "value2");
Iterator i$ = m.keySet().iterator();
while(i$.hasNext()) {
Object key = i$.next();
if (key.equals("key2")) {
m.remove(key);
}
}
這么看應該就很容易理解了,而且這個錯誤也很容易發生。
在迭代器遍歷的過程中,會將key值為“key2”的元素移除。移除時調用的HashMap的remove方法會對modCount值+1,但是這個方法并不會同步expectedModCount的值。所以在下一次迭代器調用i$.next();方法時,會發生異常。
expectedModCount // For fast-fail:在以下方法會同步modCount值
- HashIterator的構造方法
- HashIterator的remove方法
所以將上面移除元素的代碼。替換為 i$.remove();
就可以了。
思考
關于 i++ 計算不是原子性的懷疑:
HashMap源碼記錄modCount++這個計算方式在多線程操作時如果不能保證原子性,那么豈不是也有可能觸發ConcurrentModificationException異常?
驗證過程:
1 因為HashMap的put操作會進行modCount++
2 modCount聲明時也沒有指明volatile
那么多線程put是否會造成modCount的值不準確?
相關代碼可以在 JPP/ConcurrentModificationExceptionDemo類中查看。
static void atomicTest() throws InterruptedException {
HashMap m = new HashMap();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
// System.out.println(i);
m.put(i, String.valueOf(i).hashCode());
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 10000; i < 20000; i++) {
// System.out.println(i);
m.put(i, String.valueOf(i).hashCode());
}
}
}).start();
Thread.sleep(5000);
Iterator iterator = m.keySet().iterator();
iterator.next(); // 對比modCount
}
運行的結果是,如果循環次數不多,最后可以保證modCount的數值正確。但是提升循環插入的次數,會鎖住一個線程,導致其他線程的數據沒有插入成功,但是modCount的值依然是正確的。
具體這個魂循環次數設定的閾值,我也沒有過多嘗試。至少目前我沒有因為++計算不是原子性的原因出現過fast-fail
運行結果有意外收獲:
從上圖可以看出,不僅在多線程寫入的時候modCount的值無法保證(從expectedModCount看出),而且HashMap的size也不滿足期望(因為多線程put時,兩個線程的key不重復)
為了再次證明我的猜測,可以在多線程中添加 System.out.println(i);
代碼,來達到內存同步的目的
結果不出所料:
結論
1 HashMap多線程讀寫時可能會拋出ConcurrentModificationException異常,這是fast-fail快速失敗機制。
2 fast-fail實現的原理是判斷modCount和expectedModCount是否相等
3 modCount++在多線程操作時無法保證原子性,甚至HashMap整個put方法都出現了問題
PS:所以在JDK1.7的ConcurrentHashMap中出現大量 UNSAFE 和 volatile 關鍵字。
最后
上文所有代碼片段都是基于JDK1.7,雖然JDK1.8中對HashMap做了較大的改動。但是文章的思路和結論都是相同的。