線程不安全的HashMap
效率低下的HashTable容器
HashMap,HashTable,ConcurrentHashMap
先說這三個Map的區別:
HashTable
- 底層數組+鏈表實現,無論key還是value都不能為null,線程安全,實現線程安全的方式是在修改數據時鎖住整個HashTable,效率低,ConcurrentHashMap做了相關優化
- 初始size為11,擴容:newsize = olesize*2+1
- 計算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
HashMap
- 底層數組+鏈表實現,可以存儲null鍵和null值,線程不安全
- 初始size為16,擴容:newsize = oldsize*2,size一定為2的n次冪
- 擴容針對整個Map,每次擴容時,原來數組中的元素依次重新計算存放位置,並重新插入
- 插入元素后才判斷該不該擴容,有可能無效擴容(插入后如果擴容,如果沒有再次插入,就會產生無效擴容)
- 當Map中元素總數超過Entry數組的75%,觸發擴容操作,為了減少鏈表長度,元素分配更均勻
- 計算index方法:index = hash & (tab.length – 1)
HashMap的初始值還要考慮加載因子:
- 哈希沖突:若干Key的哈希值按數組大小取模后,如果落在同一個數組下標上,將組成一條Entry鏈,對Key的查找需要遍歷Entry鏈上的每個元素執行equals()比較。
- 加載因子:為了降低哈希沖突的概率,默認當HashMap中的鍵值對達到數組大小的75%時,即會觸發擴容。因此,如果預估容量是100,即需要設定100/0.75=134的數組大小。
- 空間換時間:如果希望加快Key查找的時間,還可以進一步降低加載因子,加大初始大小,以降低哈希沖突的概率。
HashMap和Hashtable都是用hash算法來決定其元素的存儲,因此HashMap和Hashtable的hash表包含如下屬性:
- 容量(capacity):hash表中桶的數量
- 初始化容量(initial capacity):創建hash表時桶的數量,HashMap允許在構造器中指定初始化容量
- 尺寸(size):當前hash表中記錄的數量
- 負載因子(load factor):負載因子等於“size/capacity”。負載因子為0,表示空的hash表,0.5表示半滿的散列表,依此類推。輕負載的散列表具有沖突少、適宜插入與查詢的特點(但是使用Iterator迭代元素時比較慢)
除此之外,hash表里還有一個“負載極限”,“負載極限”是一個0~1的數值,“負載極限”決定了hash表的最大填滿程度。當hash表中的負載因子達到指定的“負載極限”時,hash表會自動成倍地增加容量(桶的數量),並將原有的對象重新分配,放入新的桶內,這稱為rehashing。
HashMap和Hashtable的構造器允許指定一個負載極限,HashMap和Hashtable默認的“負載極限”為0.75,這表明當該hash表的3/4已經被填滿時,hash表會發生rehashing。
“負載極限”的默認值(0.75)是時間和空間成本上的一種折中:
- 較高的“負載極限”可以降低hash表所占用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的操作(HashMap的get()與put()方法都要用到查詢)
- 較低的“負載極限”會提高查詢數據的性能,但會增加hash表所占用的內存開銷
程序猿可以根據實際情況來調整“負載極限”值。
ConcurrentHashMap
- 底層采用分段的數組+鏈表實現,線程安全
- 通過把整個Map分為N個Segment,可以提供相同的線程安全,但是效率提升N倍,默認提升16倍。(讀操作不加鎖,由於HashEntry的value變量是 volatile的,也能保證讀取到最新的值。)
- Hashtable的synchronized是針對整張Hash表的,即每次鎖住整張表讓線程獨占,ConcurrentHashMap允許多個修改操作並發進行,其關鍵在於使用了鎖分離技術
- 有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖
- 擴容:段內擴容(段內元素超過該段對應Entry數組長度的75%觸發擴容,不會對整個Map進行擴容),插入前檢測需不需要擴容,有效避免無效擴容
Hashtable和HashMap都實現了Map接口,但是Hashtable的實現是基於Dictionary抽象類的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴展性更好。
HashMap基於哈希思想,實現對數據的讀寫。當我們將鍵值對傳遞給put()方法時,它調用鍵對象的hashCode()方法來計算hashcode,然后找到bucket位置來存儲值對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當發生碰撞時,對象將會儲存在鏈表的下一個節點中。HashMap在每個鏈表節點中儲存鍵值對對象。當兩個不同的鍵對象的hashcode相同時,它們會儲存在同一個bucket位置的鏈表中,可通過鍵對象的equals()方法來找到鍵值對。如果鏈表大小超過閾值(TREEIFY_THRESHOLD,8),鏈表就會被改造為樹形結構。
在HashMap中,null可以作為鍵,這樣的鍵只有一個,但可以有一個或多個鍵所對應的值為null。當get()方法返回null值時,即可以表示HashMap中沒有該key,也可以表示該key所對應的value為null。因此,在HashMap中不能由get()方法來判斷HashMap中是否存在某個key,應該用containsKey()方法來判斷。而在Hashtable中,無論是key還是value都不能為null。
Hashtable是線程安全的,它的方法是同步的,可以直接用在多線程環境中。而HashMap則不是線程安全的,在多線程環境中,需要手動實現同步機制。
Hashtable與HashMap另一個區別是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它線程改變了HashMap的結構(增加或者移除元素),將會拋出ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會拋出ConcurrentModificationException異常。但這並不是一個一定發生的行為,要看JVM。
先看一下簡單的類圖:
從類圖中可以看出來在存儲結構中ConcurrentHashMap比HashMap多出了一個類Segment,而Segment是一個可重入鎖。
ConcurrentHashMap是使用了鎖分段技術來保證線程安全的。
鎖分段技術:首先將數據分成一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。
ConcurrentHashMap提供了與Hashtable和SynchronizedMap不同的鎖機制。Hashtable中采用的鎖機制是一次鎖住整個hash表,從而在同一時刻只能由一個線程對其進行操作;而ConcurrentHashMap中則是一次鎖住一個桶。
ConcurrentHashMap默認將hash表分為16個桶,諸如get、put、remove等常用操作只鎖住當前需要用到的桶。這樣,原來只能一個線程進入,現在卻能同時有16個寫線程執行,並發性能的提升是顯而易見的。
===========================================>
鎖分段技術

二、應用場景
三、源碼解讀
/** * The segments, each of which is a specialized hash table */ final Segment<K,V>[] segments;
不變(Immutable)和易變(Volatile)
static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; }
其它
定位操作:
final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }
final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }
數據結構
static final class Segment<K,V> extends ReentrantLock implements Serializable { /** * The number of elements in this segment's region. */ transient volatileint count; /** * Number of updates that alter the size of the table. This is * used during bulk-read methods to make sure they see a * consistent snapshot: If modCounts change during a traversal * of segments computing size or checking containsValue, then * we might have an inconsistent view of state so (usually) * must retry. */ transient int modCount; /** * The table is rehashed when its size exceeds this threshold. * (The value of this field is always <tt>(int)(capacity * * loadFactor)</tt>.) */ transient int threshold; /** * The per-segment table. */ transient volatile HashEntry<K,V>[] table; /** * The load factor for the hash table. Even though this value * is same for all segments, it is replicated to avoid needing * links to outer object. * @serial */ final float loadFactor; }
刪除操作remove(key)
public V remove(Object key) { hash = hash(key.hashCode()); return segmentFor(hash).remove(key, hash, null); }
V remove(Object key, int hash, Object value) { lock(); try { int c = count - 1; HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) { V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned. ++modCount; HashEntry<K,V> newFirst = e.next; *for (HashEntry<K,V> p = first; p != e; p = p.next) *newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile } } return oldValue; } finally { unlock(); } }


get操作
V get(Object key, int hash) { if (count != 0) { // read-volatile 當前桶的數據個數是否為0 HashEntry<K,V> e = getFirst(hash); 得到頭節點 while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck } e = e.next; } } returnnull; }
V readValueUnderLock(HashEntry<K,V> e) { lock(); try { return e.value; } finally { unlock(); } }
put操作
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile } return oldValue; } finally { unlock(); } }
- 是否需要擴容。在插入元素前會先判斷Segment里的HashEntry數組是否超過容量(threshold),如果超過閥值,數組進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素后判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之后沒有新元素插入,這時HashMap就進行了一次無效的擴容。
- 如何擴容。擴容的時候首先會創建一個兩倍於原容量的數組,然后將原數組里的元素進行再hash后插入到新的數組里。為了高效ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容。
boolean containsKey(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) returntrue; e = e.next; } } returnfalse; }