不多說,直接上干貨!
這篇我是從整體出發去寫的。
牛客網Java刷題知識點之Java 集合框架的構成、集合框架中的迭代器Iterator、集合框架中的集合接口Collection(List和Set)、集合框架中的Map集合
牛客網Java刷題知識點之Map的兩種取值方式keySet和entrySet、HashMap 、Hashtable、TreeMap、LinkedHashMap、ConcurrentHashMap 、WeakHashMap
首先,HashMap不支持線程的同步。
同步,指的是在一個時間點只能有一個線程可以修改hash表,任何線程在執行Hashtable的更新操作前都需要獲取對象鎖,其他線程則等待鎖的釋放。
為什么HashMap是線程不安全的,實際會如何體現?
第一,如果多個線程同時使用put方法添加元素。
假設正好存在兩個put的key發生了碰撞(hash值一樣),那么根據HashMap的實現,這兩個key會添加到數組的同一個位置,這樣最終就會發生其中一個線程的put的數據被覆蓋。
第二,如果多個線程同時檢測到元素個數超過數組大小*loadFactor。
這樣會發生多個線程同時對hash數組進行擴容,都在重新計算元素位置以及復制數據,但是最終只有一個線程擴容后的數組會賦給table,也就是說其他線程的都會丟失,並且各自線程put的數據也丟失。且會引起死循環的錯誤。
HashMap底層是一個Entry數組,當發生hash沖突的時候,hashmap是采用鏈表的方式來解決的,在對應的數組位置存放鏈表的頭結點。對鏈表而言,新加入的節點會從頭結點加入。
我們來分析一下多線程訪問:
1、在hashmap做put操作的時候會調用下面方法:
// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。 void addEntry(int hash, K key, V value, int bucketIndex) { // 保存“bucketIndex”位置的值到“e”中 Entry<K,V> e = table[bucketIndex]; // 設置“bucketIndex”位置的元素為“新Entry”, // 設置“e”為“新Entry的下一個節點” table[bucketIndex] = new Entry<K,V>(hash, key, value, e); // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); }
在hashmap做put操作的時候會調用到以上的方法。現在假如A線程和B線程同時對同一個數組位置調用addEntry,兩個線程會同時得到現在的頭結點,然后A寫入新的頭結點之后,B也寫入新的頭結點,那B的寫入操作就會覆蓋A的寫入操作造成A的寫入操作丟失。
2、刪除鍵值對的代碼
<span style="font-size: 18px;"> </span>// 刪除“鍵為key”的元素 final Entry<K,V> removeEntryForKey(Object key) { // 獲取哈希值。若key為null,則哈希值為0;否則調用hash()進行計算 int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); Entry<K,V> prev = table[i]; Entry<K,V> e = prev; // 刪除鏈表中“鍵為key”的元素 // 本質是“刪除單向鏈表中的節點” while (e != null) { Entry<K,V> next = e.next; Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; }
當多個線程同時操作同一個數組位置的時候,也都會先取得現在狀態下該位置存儲的頭結點,然后各自去進行計算操作,之后再把結果寫會到該數組位置去,其實寫回的時候可能其他的線程已經就把這個位置給修改過了,就會覆蓋其他線程的修改。
3、addEntry中當加入新的鍵值對后鍵值對總數量超過門限值的時候會調用一個resize操作,代碼如下:
// 重新調整HashMap的大小,newCapacity是調整后的容量 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; //如果就容量已經達到了最大值,則不能再擴容,直接返回 if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } // 新建一個HashMap,將“舊HashMap”的全部元素添加到“新HashMap”中, // 然后,將“新HashMap”賦值給“舊HashMap”。 Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
這個操作會新生成一個新的容量的數組,然后對原數組的所有鍵值對重新進行計算和寫入新的數組,之后指向新生成的數組。
當多個線程同時檢測到總數量超過門限值的時候就會同時調用resize操作,各自生成新的數組並rehash后賦給該map底層的數組table,結果最終只有最后一個線程生成的新數組被賦給table變量,其他線程的均會丟失。而且當某些線程已經完成賦值而其他線程剛開始的時候,就會用已經被賦值的table作為原始數組,這樣也會有問題。
如何實現HashMap的同步?
答:
第一種方法:
直接使用Hashtable,但是當一個線程訪問HashTable的同步方法時,其他線程如果也要訪問同步方法,會被阻塞住。舉個例子,當一個線程使用put方法時,另一個線程不但不可以使用put方法,連get方法都不可以,效率很低,現在基本不會選擇它了。
第二種方法: HashMap可以通過下面的語句進行同步,
Collections.synchronizeMap(hashMap);
HashMap可以通過Map m = Collections.synchronizedMap(new HashMap())來達到同步的效果。
具體而言,該方法返回一個同步的Map,該Map封裝了底層的HashMap的所有方法,使得底層的HashMap即使是在多線程的環境中也是安全的。
第三種方法:
直接使用JDK 5 之后的 ConcurrentHashMap,如果使用Java 5或以上的話,請使用ConcurrentHashMap。
Collections.synchronizeMap(hashMap); 又是如何保證了HashMap線程安全?
// synchronizedMap方法 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) { return new SynchronizedMap<>(m); } // SynchronizedMap類 private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) { this.m = Objects.requireNonNull(m); mutex = this; } SynchronizedMap(Map<K,V> m, Object mutex) { this.m = m; this.mutex = mutex; } public int size() { synchronized (mutex) {return m.size();} } public boolean isEmpty() { synchronized (mutex) {return m.isEmpty();} } public boolean containsKey(Object key) { synchronized (mutex) {return m.containsKey(key);} } public boolean containsValue(Object value) { synchronized (mutex) {return m.containsValue(value);} } public V get(Object key) { synchronized (mutex) {return m.get(key);} } public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} } public V remove(Object key) { synchronized (mutex) {return m.remove(key);} } // 省略其他方法 }
從源碼中看出 synchronizedMap()方法返回一個SynchronizedMap類的對象,而在SynchronizedMap類中使用了synchronized來保證對Map的操作是線程安全的,故效率其實也不高。