淺談HashMap與線程安全 (JDK1.8)

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

之因此採起這樣的數據結構存儲數據是爲了防止衝突發生: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)。git

而對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)github

Hashtable 源碼中是使用 synchronized 來保證線程安全的,好比下面的 get 方法和 put 方法:
public synchronized V get(Object key) {...}
public synchronized V put(K key, V value) {...}

因此當一個線程訪問 HashTable 的同步方法時,其餘線程若是也要訪問同步方法,會被阻塞住。所以Hashtable效率很低,基本被廢棄。bootstrap

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 。
 
相關文章
相關標籤/搜索