HashMap是Java程序員使用頻率最高的用於映射(鍵值對)處理的數據類型。HashMap 繼承自 AbstractMap 是基於哈希表的 Map 接口的實現,以 Key-Value 的形式存在,即存儲的對象是 Entry (同時包含了 Key 和 Value)
本文所有源碼都是基於JDK1.8的,不同版本的代碼差異可以自行查閱官方文檔。
HashMap源碼(JDK1.8):
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; // .... } /** * The table, initialized on first use, and resized as * necessary. When allocated, length is always a power of two. * (We also tolerate length zero in some operations to allow * bootstrapping mechanics that are currently not needed.) */ transient Node<K,V>[] table; //.... }
HashMap 內部存儲使用了一個 Node 數組(默認大小是16),每個Node都是一個鏈表。每個鏈表存儲相同索引的元素。
之所以采取這樣的數據結構存儲數據是為了防止沖突發生:Java中兩個不同的對象可能有一樣的hashCode,所以不同的鍵可能有一樣hashCode,從而導致沖突的產生。
static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6;
從Java 8開始,HashMap(ConcurrentHashMap以及LinkedHashMap)在處理頻繁沖突時,為了提升性能將使用平衡樹來代替鏈表,當同一hash桶中的元素數量超過特定的值(TREEIFY_THRESHOLD )便會由鏈表切換到平衡樹,這會將get()方法的性能從O(n)提高到O(logn)。
而對HashMap進行split操作而生成元素數量在特定的值或以下時,平衡樹會被重新轉化成鏈表。
HashMap的自動擴容機制
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap 內部的 Node 數組默認的大小是16(DEFAULT_INITIAL_CAPACITY )。
假設有1萬個元素需要放入HashMap,那么最好的情況下每個 hash 桶里都有625個元素(每625個元素共用一個索引)。此時你要調用put()、get()、remove()等方法去操作某一個元素,平均要遍歷313個元素,效率大大降低。
為了解決這個問題,HashMap 提供了自動擴容機制,當元素個數達到 數組大小 × 負載因子 的數量后會擴大數組的大小(最長鏈表的Entry個數 > threshold)。在默認情況下,數組大小為16,因子(DEFAULT_LOAD_FACTOR )為0.75,也就是說當 HashMap 中的元素超過16*0.75=12時,會把數組大小擴展為2*16=32,並且重新分配索引,計算每個元素在新數組中的位置。
線程不安全
HashMap 在並發時可能出現的問題主要是兩方面:
1. put的時候導致的多線程數據不一致
比如有兩個線程A和B,首先A希望插入一個key-value對到HashMap中,首先計算記錄所要落到的 hash桶的索引坐標,然后獲取到該桶里面的鏈表頭結點,此時線程A的時間片用完了,而此時線程B被調度得以執行,和線程A一樣執行,只不過線程B成功將記錄插到了桶里面,假設線程A插入的記錄計算出來的 hash桶索引和線程B要插入的記錄計算出來的 hash桶索引是一樣的,那么當線程B成功插入之后,線程A再次被調度運行時,它依然持有過期的鏈表頭但是它對此一無所知,以至於它認為它應該這樣做,如此一來就覆蓋了線程B插入的記錄,這樣線程B插入的記錄就憑空消失了,造成了數據不一致的行為。
2. resize而引起死循環(JDK1.8已經不會出現該問題)
這種情況發生在JDK1.7 中HashMap自動擴容時,當2個線程同時檢測到元素個數超過 數組大小 × 負載因子。此時2個線程會在put()方法中調用了resize(),兩個線程同時修改一個鏈表結構會產生一個循環鏈表(JDK1.7中,會出現resize前后元素順序倒置的情況)。接下來再想通過get()獲取某一個元素,就會出現死循環。
線程安全的Map
- Hashtable
- ConcurrentHashMap
- Synchronized Map
//Hashtable Map<String, String> hashtable = new Hashtable<>(); //synchronizedMap Map<String, String> synchronizedHashMap = Collections.synchronizedMap(new HashMap<String, String>()); //ConcurrentHashMap Map<String, String> concurrentHashMap = new ConcurrentHashMap<>();
Hashtable (deprecate)
Hashtable 源碼中是使用
synchronized 來保證線程安全的,比如下面的 get 方法和 put 方法:
public synchronized V get(Object key) {...} public synchronized V put(K key, V value) {...}
所以當一個線程訪問 HashTable 的同步方法時,其他線程如果也要訪問同步方法,會被阻塞住。因此Hashtable效率很低,基本被廢棄。
ConcurrentHashMap
ConcurrentHashMap沿用了與它同時期的HashMap版本的思想,底層依然由“數組”+鏈表+紅黑樹的方式思想,但是為了做到並發,又增加了很多輔助的類,例如TreeBin,Traverser等對象內部類。
且與hashtable不同的是:
ConcurrentHashMap沒有對整個hash表進行鎖定,而是采用了分離鎖(segment)的方式進行局部鎖定。具體體現在,它在代碼中維護着一個segment數組。
/** For serialization compatibility. */ private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("segments", Segment[].class), new ObjectStreamField("segmentMask", Integer.TYPE), new ObjectStreamField("segmentShift", Integer.TYPE) };
它增加了一個的屬性——sizeCtl:
hash表初始化或擴容時的一個控制位標識量。
負數代表正在進行初始化或擴容操作 -1代表正在初始化 -N 表示有N-1個線程正在進行擴容操作 正數或0代表hash表還沒有被初始化,這個數值表示初始化或下一次進行擴容的大小
/** * Table initialization and resizing control. When negative, the * table is being initialized or resized: -1 for initialization, * else -(1 + the number of active resizing threads). Otherwise, * when table is null, holds the initial table size to use upon * creation, or 0 for default. After initialization, holds the * next element count value upon which to resize the table. */ private transient volatile int sizeCtl; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; public final V setValue(V value) { throw new UnsupportedOperationException(); } } /** * Virtualized support for map.get(); overridden in subclasses. */ Node<K,V> find(int h, Object k) { Node<K,V> e = this; if (k != null) { do { K ek; if (e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)))) return e; } while ((e = e.next) != null); } return null; }
在ConcurrentHashMap的Node內部類中,它對val和next屬性設置了volatile同步鎖,不允許調用setValue方法直接改變Node的value域,增加了find方法輔助map.get()方法。
SynchronizedMap
SynchronizedMap是Collectionis的內部類。
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable { private final Map<K,V> m; // Backing Map final Object mutex; // Object on which to synchronize 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);} } public void putAll(Map<? extends K, ? extends V> map) { synchronized (mutex) {m.putAll(map);} } public void clear() { synchronized (mutex) {m.clear();} } //... }
在 SynchronizedMap 類中使用了 synchronized 同步關鍵字來保證對 Map 的操作是線程安全的。
三者的效率對比:
分別通過三種方式創建 Map 對象,使用
ExecutorService 來並發運行5個線程,每個線程添加/獲取500K個元素,比較其用時多少。
代碼就不貼了,詳見
這里
ConcurrentHashMap明顯優於Hashtable和SynchronizedMap 。
