轉載一個關於HashMap不錯的講解,作者通過圖形的方式很生動的把hashmap的源碼解釋出來。
http://www.cnblogs.com/ITtangtang/p/3948406.html
下面是本人對HashMap源碼的理解:
一、HashTable與HashMap的區別:
1.1 歷史
HashMap的出現時間要晚於HashTable。
HashTable在JDK1.1 版本時就已經出現,之後在JDK1.2 纔出現了HashMap。
1.2 實現的接口
從圖中可以看出,
- 兩者都實現了Map、Cloneable、Seriallizable接口
- 但繼承的抽象類是不一樣的, HashMap 繼承了AbstractMap 而 HashTable 繼承的是已經 廢棄 的 Dictionary 類
- 從兩者提供的方法可以看出,這兩個類的功能是一樣的,都可以對鍵值對進行增刪改查,遍歷,序列化和拷貝。
1.2.1 關於空的鍵值對
//HashTable關於put的源碼及註釋
public synchronized V put(K key, V value) {
// 如果value爲null,拋出NullPointerException
if (value == null) {
throw new NullPointerException();
}
// 如果key爲null,在調用key.hashCode()時拋出NullPointerException
// ...
}
//HasMap關於put的源碼及註釋
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 當key爲null時,調用putForNullKey特殊處理
if (key == null)
return putForNullKey(value);
// ...
}
private V putForNullKey(V value) {
// key爲null時,放到table[0]也就是第0個bucket中
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
通過源碼我們看出, HashMap 是支持 空的(null)鍵值對 的,當遇到null時將null轉化成hashCode值0,而 HashTable 在遇到null時,會拋出 NullPointerException 異常。
1.3 實現的原理
下面我們深入到數據結構和算法層來分析一下兩者間的區別。
1.3.1 數據結構
HashTable 和 HashMap 在數據結構上兩者是相同的,都使用哈希表來存儲鍵值對,繼承自Map.Entry中的Entry類,每一個Entry對象對應哈希表中的一個鍵值對。
散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它通過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫做散列函數,存放記錄的數組叫做散列表。
給定表M,存在函數f(key),對任意給定的關鍵字值key,代入函數後若能得到包含該關鍵字的記錄在表中的地址,則稱表M爲哈希(Hash)表,函數f(key)爲哈希(Hash) 函數。
//Map.Entry源碼
interface Entry<K,V> {
K getKey();
V getValue();
V setValue(V value);
boolean equals(Object o);
int hashCode();
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getValue().compareTo(c2.getValue());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
}
public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
Objects.requireNonNull(cmp);
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
}
}
可以說有多少的 鍵值對 就有多少的 Entry對象
下圖說明HashTable 和 HashMap 如何存儲鍵值對。
上圖可以看出,哈希值相等的話,entry對象就以鏈表(HashMap在JDK1.8之後優化爲紅黑樹)的形式進行存儲。
//HashTable
private transient Entry<K,V>[] table;
//HashMap
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
得出結論HashTable 和 HashMap 內部使用 Entry數組 進行 鍵值對 的存儲。
1.3.2 算法
下面一部分我們研究一下 HashTable 和 HashMap 在底層初始化和將給定的key映射到對應hash值上的算法,從而發現兩者之間的異同。
初始化與擴容:
// HashTable
// 哈希表默認初始大小爲11
public Hashtable() {
this(11, 0.75f);
}
protected void rehash() {
int oldCapacity = table.length;
Entry<K,V>[] oldMap = table;
// 每次擴容爲原來的2n+1
int newCapacity = (oldCapacity << 1) + 1;
// ...
}
// HashMap
// 哈希表默認初始大小爲2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
void addEntry(int hash, K key, V value, int bucketIndex) {
// 每次擴充爲原來的2n
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
}
從源碼我們發現,HashTable 每次初始化爲11,每次擴容爲2n+1。而 HashMap 初始化爲16,每次擴容爲原來的2倍。還有一點就是,HashTable 如果你給定大小,那麼其初始化時將按照你 給定的大小 來確定容量,但是HashMap 的大小爲你給定大小的 2的冪次方。
在效率上HashMap 的方式直接使用位運算來得到結果更高效,但會因此造成hash表分佈的不均勻,
如何定位到 hash桶:
// HashTable
// hash 不能超過Integer.MAX_VALUE 所以要取其最小的31個bit
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
// 直接計算key.hashCode()
private int hash(Object k) {
// hashSeed will be zero if alternative hashing is disabled.
return hashSeed ^ k.hashCode();
}
// HashMap
int hash = hash(key);
int i = indexFor(hash, table.length);
// 在計算了key.hashCode()之後,做了一些位運算來減少哈希衝突
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 取模不再需要做除法
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 :
// "length must be a non-zero power of 2";
return h & (length-1);
}
1.4 線程安全
//HashTable
public synchronized V get(Object key) {
Entry tab[] = table;
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return e.value;
}
}
return null;
}
public Set<K> keySet() {
if (keySet == null)
keySet = Collections.synchronizedSet(new KeySet(), this);
return keySet;
}
通過上面關於 HashTable 的源碼我們發現,HashTable公開的方法,比如get都使用了 synchronized 描述符。而遍歷視圖,比如keySet都使用了Collections.synchronizedXXX 進行了同步包裝。在多線程狀態下是同步的,而 HashMap 是不同步的。
1.5 HashTable已經被淘汰
以下描述來自於HashTable的類註釋:
If a thread-safe implementation is not needed, it is recommended to
use HashMap in place of Hashtable. If a thread-safe highly-concurrent
implementation is desired, then it is recommended to use
java.util.concurrent.ConcurrentHashMap in place of Hashtable
在不考慮線程安全的情況下,就使用HashMap,反之使用ConcurrentHashMap。HashTable已經被淘汰了,不要在新的代碼中再使用它。
二、ConcurrentHashMap與HashMap的區別:
2.1 ConcurrentHashMap
對於一般的增刪改查,ConcurrentHashMap 的設計思路和 HashMap 是基本相同的,只是,ConcurrentHashMap 由一個個 segment(部分/一段) 也可以叫做 分段鎖 。可以這麼理解,ConcurrentHashMap 由一個 segment數組 組成,每一個 segment 繼承了 ReentrantLock鎖 ,這樣可以確保每一個 segment 都是加鎖的,以保證線程安全。
2.2 ConcurrentHashMap的初始化
ConcurrentHashMap 在默認初始化的時候就擁有有 16 個 Segments,也就是說默認情況下,支持最多16 個線程併發寫入,如果初始化設置爲其他值之後,便不可以擴容了。
//initialCapacity:初始容量,這個值指的是整個 ConcurrentHashMap
//的初始容量,實際操作的時候需要平均分給每個 Segment。
//loadFactor:負載因子,由於Segment 數組不可以擴容,
//所以這個負載因子是給每個 Segment 內部使用的。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
// 計算並行級別 ssize,因爲要保持並行級別是 2 的 n 次方
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// 我們這裏先不要那麼燒腦,用默認值,concurrencyLevel 爲 16,sshift 爲 4
// 那麼計算出 segmentShift 爲 28,segmentMask 爲 15,後面會用到這兩個值
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// initialCapacity 是設置整個 map 初始的大小,
// 這裏根據 initialCapacity 計算 Segment 數組中每個位置可以分到的大小
// 如 initialCapacity 爲 64,那麼每個 Segment 或稱之爲"槽"可以分到 4 個
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
// 默認 MIN_SEGMENT_TABLE_CAPACITY 是 2,這個值也是有講究的,因爲這樣的話,對於具體的槽上,
// 插入一個元素不至於擴容,插入第二個的時候纔會擴容
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 創建 Segment 數組,
// 並創建數組的第一個元素 segment[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
// 往數組寫入 segment[0]
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
這樣 ConcurrentHashMap 便算是完成了初始化。
2.3 ConcurrentHashMap 和 HashMap 的 put 和 get 方法對比
2.3.1 ConcurrentHashMap的put方法過程
- put的第一步:
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
// 1. 計算 key 的 hash 值
int hash = hash(key);
// 2. 根據 hash 值找到 Segment 數組中的位置 j
// hash 是 32 位,無符號右移 segmentShift(28) 位,剩下低 4 位,
// 然後和 segmentMask(15) 做一次與操作,
// 也就是說 j 是 hash 值的最後 4 位,也就是槽的數組下標
int j = (hash >>> segmentShift) & segmentMask;
// 剛剛說了,初始化的時候初始化了 segment[0],但是其他位置還是 null,
// ensureSegment(j) 對 segment[j] 進行初始化
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
// 3. 插入新值到 槽 s 中
return s.put(key, hash, value, false);
}
- Segment內部初始化,由數組+鏈表組成
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 在往該 segment 寫入前,需要先獲取該 segment 的獨佔鎖
// 先看主流程,後面還會具體介紹這部分內容
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
// 這個是 segment 內部的數組
HashEntry<K,V>[] tab = table;
// 再利用 hash 值,求應該放置的數組下標
int index = (tab.length - 1) & hash;
// first 是數組該位置處的鏈表的表頭
HashEntry<K,V> first = entryAt(tab, index);
// 下面這串 for 循環雖然很長,不過也很好理解,想想該位置沒有任何元素和已經存在一個鏈表這兩種情況
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
// 覆蓋舊值
e.value = value;
++modCount;
}
break;
}
// 繼續順着鏈表走
e = e.next;
}
else {
// node 到底是不是 null,這個要看獲取鎖的過程,不過和這裏都沒有關係。
// 如果不爲 null,那就直接將它設置爲鏈表表頭;如果是null,初始化並設置爲鏈表表頭。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 如果超過了該 segment 的閾值,這個 segment 需要擴容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node); // 擴容後面也會具體分析
else
// 沒有達到閾值,將 node 放到數組 tab 的 index 位置,
// 其實就是將新的節點設置成原鏈表的表頭
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 解鎖
unlock();
}
return oldValue;
}
- ensureSegment:初始化segment
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 這裏看到爲什麼之前要初始化 segment[0] 了,
// 使用當前 segment[0] 處的數組長度和負載因子來初始化 segment[k]
// 爲什麼要用“當前”,因爲 segment[0] 可能早就擴容過了
Segment<K,V> proto = ss[0];
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
// 初始化 segment[k] 內部的數組
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // 再次檢查一遍該槽是否被其他線程初始化了。
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
// 使用 while 循環,內部用 CAS,當前線程成功設值或其他線程成功設值後,退出
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
- scanAndLockForPut:獲取寫入鎖
前面 segment 的 put 方法中,第一步就調用
node = tryLock() ? null : scanAndLockForPut(key, hash, value)
,
也就是說先進行一次 tryLock() 快速獲取該 segment 的獨佔鎖,如果失敗,那麼進入到 scanAndLockForPut 這個方法來獲取鎖。
scanAndLockForPut如何加鎖
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
// 循環獲取鎖
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
// 進到這裏說明數組該位置的鏈表是空的,沒有任何元素
// 當然,進到這裏的另一個原因是 tryLock() 失敗,所以該槽存在併發,不一定是該位置
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
// 順着鏈表往下走
e = e.next;
}
// 重試次數如果超過 MAX_SCAN_RETRIES(單核1多核64),那麼不搶了,進入到阻塞隊列等待鎖
// lock() 是阻塞方法,直到獲取鎖後返回
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
// 這個時候是有大問題了,那就是有新的元素進到了鏈表,成爲了新的表頭
// 所以這邊的策略是,相當於重新走一遍這個 scanAndLockForPut 方法
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
這個方法有兩個出口,一個是 tryLock() 成功了,循環終止,另一個就是重試次數超過了 MAX_SCAN_RETRIES,進到 lock() 方法,此方法會阻塞等待,直到成功拿到獨佔鎖。