面試的時候經(jīng)常問到产喉。這里對jdk1.7的HashMap和ConcurrentHashMap原理進(jìn)行分析矫限。下篇將詳細(xì)分析jdk1.8的實現(xiàn)彭雾。
1 HashMap
HashMap是基于哈希表的Map接口的非同步實現(xiàn)沪铭。此實現(xiàn)提供所有可選的映射操作旱易,并允許使用null值和null鍵。此類不保證映射的順序建丧,特別是它不保證該順序恒久不變(擴(kuò)容的時候會變排龄,后面會詳細(xì)說明)。
1.1 成員結(jié)構(gòu)
-
數(shù)組
采用一段連續(xù)的存儲單元來存儲數(shù)據(jù)翎朱。對于指定下標(biāo)的查找橄维,時間復(fù)雜度為O(1);通過給定值進(jìn)行查找,需要對數(shù)組進(jìn)行全部遍歷拴曲,時間復(fù)雜度為O(n)争舞;對一般的插入操作,涉及到數(shù)組元素的移動澈灼,平均復(fù)雜度為O(n)竞川。
-
線性鏈表
對于鏈表的新增、刪除操作(在找到指定操作位置后)叁熔,僅需要處理節(jié)點之間的引用即可委乌,時間復(fù)雜度為O(1);查找的時候需要遍歷整個鏈表。
-
二叉樹
對一顆平衡的有序二叉樹荣回,插入遭贸、查找、刪除心软。時間復(fù)雜度為O(logn)
-
哈希表
相比上訴幾種數(shù)據(jù)結(jié)構(gòu)革砸,在哈希表中進(jìn)行添加、刪除糯累、查找算利、性能十分的高,在不考慮hash沖突的情況下僅需要一次定位即可泳姐,時間復(fù)雜度為O(1)效拭。
數(shù)據(jù)結(jié)構(gòu)的物理存儲結(jié)構(gòu)只有兩種:順序存儲和鏈?zhǔn)酱鎯?像棧、隊列、樹缎患、圖等是從邏輯結(jié)構(gòu)去抽象的慕的,映射到內(nèi)存中只有兩種結(jié)構(gòu))。上面提到根據(jù)下標(biāo)去查找數(shù)據(jù)挤渔,一次定位就可以找到肮街,哈希表就是利用了這種特性。因此哈希表的主干就是數(shù)組判导。
hash沖突
兩個數(shù)據(jù)可能取hash之后是相同的值嫉父,這樣存儲的時候就產(chǎn)生了hash碰撞。對于解決hash碰撞有多種方案:開放地址法(發(fā)生沖突眼刃,繼續(xù)尋找下一塊未被占用的存儲地址)绕辖、再散列法、鏈地址法擂红。HashMap就是采用了鏈地址法仪际。
HashMap的主干就是一個數(shù)組(Entry),每一個Entry是一個(key,value)的鍵值對
//初始化一個Entry數(shù)組,數(shù)組的長度一定是2的次冪昵骤。
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap的一個靜態(tài)內(nèi)部類树碱,代碼如下:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;//存儲指向下一個Entry的引用,單鏈表結(jié)構(gòu)
int hash;//對key的hashcode值進(jìn)行hash運(yùn)算后得到的值变秦,存儲在Entry成榜,避免重復(fù)計算
/**
* Creates new entry.
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
如圖,HashMap主干是Entry數(shù)組伴栓,縱向是鏈表結(jié)構(gòu)伦连。
其他幾個重要字段:
//實際存儲的key-value鍵值對的個數(shù)
transient int size;
//閾值雨饺,當(dāng)table == {}時钳垮,該值為初始容量(初始容量默認(rèn)為16);當(dāng)table被填充了额港,也就是為table分配內(nèi)存空間后饺窿,threshold一般為 capacity*loadFactory。HashMap在進(jìn)行擴(kuò)容時需要參考threshold移斩,后面會詳細(xì)談到
int threshold;
//負(fù)載因子肚医,代表了table的填充度有多少,默認(rèn)是0.75
final float loadFactor;
//用于快速失敗向瓷,由于HashMap非線程安全肠套,在對HashMap進(jìn)行迭代時,如果期間其他線程的參與導(dǎo)致HashMap的結(jié)構(gòu)發(fā)生變化了(比如put猖任,remove等操作)你稚,需要拋出異常ConcurrentModificationException
transient int modCount;
1.2 構(gòu)造函數(shù)
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();//init方法在HashMap中沒有實際實現(xiàn),不過在其子類如 linkedHashMap中就會有對應(yīng)實現(xiàn)
}
1.3 put()原理
通過上面的構(gòu)造函數(shù)發(fā)現(xiàn),在初始化的時候沒有個table分配內(nèi)存刁赖,而是在put的時候才初始化分配內(nèi)存搁痛。
public V put(K key, V value) {
//如果table數(shù)組為空數(shù)組,進(jìn)行數(shù)組填充(為table分配實際內(nèi)存空間)宇弛,
//入?yún)閠hreshold鸡典,此時threshold為initialCapacity 默認(rèn)是1<<4(16)
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key為null,存儲位置為table[0]或table[0]的沖突鏈上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//對key的hashcode進(jìn)一步計算枪芒,確保散列均勻
int i = indexFor(hash, table.length);//獲取在table中的實際位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果該對應(yīng)數(shù)據(jù)已存在彻况,執(zhí)行覆蓋操作。用新value替換舊value病苗,并返回舊value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保證并發(fā)訪問時疗垛,若HashMap內(nèi)部結(jié)構(gòu)發(fā)生變化,快速響應(yīng)失敗
addEntry(hash, key, value, i);//新增一個entry
return null;
}
先來看看inflateTable這個方法
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
//此處為threshold賦值硫朦,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值贷腕,
//capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大于1
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
保證數(shù)組的長度是2的次冪
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
對key進(jìn)行hash盡量讓key分散咬展,避免hash碰撞
//這是一個神奇的函數(shù)泽裳,用了很多的異或,移位等運(yùn)算破婆,
//對key的hashcode進(jìn)一步進(jìn)行計算以及二進(jìn)制位的調(diào)整等來保證最終獲取的存儲位置盡量分布均勻
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
計算新插入數(shù)據(jù)的數(shù)組下標(biāo)
/**
* 返回數(shù)組下標(biāo)
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
我們可以想到的是取模運(yùn)算就能分散到數(shù)組中涮总,這里使用與運(yùn)算(&),因為與運(yùn)算比取牡灰ǎ快瀑梗。
為什么他們會相等?
如下裳扯,假設(shè)數(shù)組長度是16抛丽,當(dāng)前的hash(k)=17,取模計算后是index=1饰豺。這時候length-1=15,二進(jìn)制運(yùn)算如下亿鲜,算出來的也是index=1。
0001 0001
& 0000 1111
---------------------
0000 0001 = 1
這就是HashMap在put的時候下標(biāo)確定流程
再來看看addEntry的實現(xiàn):
void addEntry(int hash, K key, V value, int bucketIndex) {
//當(dāng)size超過臨界閾值threshold冤吨,并且即將發(fā)生哈希沖突時進(jìn)行擴(kuò)容
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
1.4 擴(kuò)容原理
繼續(xù)看看resize方法
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
//把舊數(shù)據(jù)移入新的table中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
繼續(xù)看transfer方法
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//for循環(huán)中的代碼蒿柳,逐個遍歷鏈表,重新計算索引位置,
//將老數(shù)組數(shù)據(jù)復(fù)制到新數(shù)組中去(數(shù)組不存儲實際數(shù)據(jù)漩蟆,所以僅僅是拷貝引用而已)
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//將當(dāng)前entry的next鏈指向新的索引位置,newTable[i]有可能為空垒探,
//有可能也是個entry鏈,如果是entry鏈怠李,直接在鏈表頭部插入圾叼。
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
transfer方法是在擴(kuò)容的時候執(zhí)行仔引,把舊的table中的數(shù)據(jù)拷貝到新的table中。
為何HashMap的數(shù)組長度一定是2的次冪褐奥?
indexFor的時候是使用h&(length-1),這時候數(shù)組的長度是2的次冪咖耘。假如是16,這時候二進(jìn)制是0001 0000撬码,length-1后就是 0000 1111儿倒,做與運(yùn)算的時候就只與低位有關(guān)。擴(kuò)容后是32呜笑,length-1后是0001 1111夫否,這樣與
h求與運(yùn)算就不會改變原來數(shù)據(jù)在數(shù)組中的位置。
總結(jié)就三個原因:
- 在計算數(shù)組下標(biāo)的時候更快速
- h的高位對數(shù)組位置計算不產(chǎn)生影響
- 擴(kuò)容的時候叫胁,不會改變原來已經(jīng)散列好的數(shù)據(jù)在數(shù)組中的位置
1.5 get()原理
public V get(Object key) {
//如果key為null,則直接去table[0]處去檢索即可凰慈。
if (key == null)
return getForNullKey();
Entry<K,V> entry = getEntry(key);
return null == entry ? null : entry.getValue();
}
get方法通過key值返回對應(yīng)value,如果key為null驼鹅,直接去table[0]處檢索微谓。我們再看一下getEntry這個方法
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
//通過key的hashcode值計算hash值
int hash = (key == null) ? 0 : hash(key);
//indexFor (hash&length-1) 獲取最終數(shù)組索引,然后遍歷鏈表输钩,通過equals方法比對找出對應(yīng)記錄
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
可以看出get方法是很簡單的豺型,具體流程就是先計算key的hash值,然后找到對應(yīng)的table[i],然后遍歷鏈表找到hash值相同的Entry,返回e.value()买乃。
2 ConcurrentHashMap
在理解了HashMap的原理之后再來理解ConcurrentHashMap就簡單很多姻氨。
1.1 成員結(jié)構(gòu)
由 segment數(shù)組和多個HashEntry組成,如下圖所示
segment數(shù)組的意義是將一個大的table分割成多個小的table,使用的是鎖分離技術(shù)剪验。每個segment存儲的是HashEntry數(shù)組+鏈表肴焊,這個就和HashMap的數(shù)據(jù)存儲一樣
初始化
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
初始化的時候需要初始化segment的容量
1.2 put()原理
ConcurrentHashMap在進(jìn)行數(shù)據(jù)插入的時候需要兩次hash去定位數(shù)據(jù)的存儲位置
/**
* map的put方法,定位segment
*/
public V put(K key, V value) {
Segment<K,V> s;
// value不能為空
if (value == null)
throw new NullPointerException();
// 獲取hash
int hash = hash(key);
// 定位segments 數(shù)組的位置
int j = (hash >>> segmentShift) & segmentMask;
// 獲取這個segment
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
// 為null 初始化當(dāng)前位置的segment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
/**
* put到table方法
*/
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 是否獲取鎖,失敗自旋獲取鎖(直到成功)
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
// 定義位置
int index = (tab.length - 1) & hash;
// 獲取第一個桶的第一個元素
// entryAt 底層調(diào)用getObjectVolatile 具有volatile讀語義
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) { // 證明鏈?zhǔn)浇Y(jié)構(gòu)有數(shù)據(jù) 遍歷節(jié)點數(shù)據(jù)替換,直到e=null
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) { // 找到了相同的key
oldValue = e.value;
if (!onlyIfAbsent) { // 默認(rèn)值false
e.value = value; // 替換value
++modCount;
}
break; // 結(jié)束循環(huán)
}
e = e.next;
}
else { // e=null (1) 之前沒有數(shù)據(jù) (2) 沒有找到替換的元素
// node是否為空,這個獲取鎖的是有關(guān)系的
// (1) node不為null,設(shè)置node的next為first
// (2) node為null,創(chuàng)建頭節(jié)點,指定next為first
if (node != null)
// 底層使用 putOrderedObject 方法 具有volatile寫語義
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 擴(kuò)容條件 (1)entry數(shù)量大于閾值 (2) 當(dāng)前table的數(shù)量小于最大容量 滿足以上條件就擴(kuò)容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
// 擴(kuò)容方法,方法里面具體講
rehash(node);
else
// 給table的index位置設(shè)置為node,
// node為頭結(jié)點,原來的頭結(jié)點first為node的next節(jié)點
// 底層也是調(diào)用的 putOrderedObject 方法 具有volatile寫語義
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
segment繼承自ReentrantLock,因此帶有鎖的功能功戚,當(dāng)在put數(shù)據(jù)的時候娶眷,
1:先獲取對應(yīng)segment的位置(第一次hash)
2:獲取segment的鎖,如果獲取到鎖疫铜,再計算在HashEntry數(shù)組中的位置(第二次hash)茂浮,添加到鏈表的尾部双谆。
1.3 get()原理
ConcurrentHashMap的get操作跟HashMap類似壳咕,只是ConcurrentHashMap第一次需要經(jīng)過一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry顽馋,遍歷該HashEntry下的鏈表進(jìn)行對比谓厘,成功就返回,不成功就返回null
/**
* get 方法
*/
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; // 獲取segment的位置
// getObjectVolatile getObjectVolatile語義讀取最新的segment,獲取table
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// getObjectVolatile getObjectVolatile語義讀取最新的hashEntry,并遍歷
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
// 找到相同的key 返回
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
1.4 擴(kuò)容原理
/**
*擴(kuò)容方法
*/
private void rehash(HashEntry<K,V> node) {
// 舊的table
HashEntry<K,V>[] oldTable = table;
// 舊的table的長度
int oldCapacity = oldTable.length;
// 擴(kuò)容原來capacity的一倍
int newCapacity = oldCapacity << 1;
// 新的閾值
threshold = (int)(newCapacity * loadFactor);
// 新的table
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
// 新的掩碼
int sizeMask = newCapacity - 1;
// 遍歷舊的table
for (int i = 0; i < oldCapacity ; i++) {
// table中的每一個鏈表元素
HashEntry<K,V> e = oldTable[i];
if (e != null) { // e不等于null
HashEntry<K,V> next = e.next; // 下一個元素
int idx = e.hash & sizeMask; // 重新計算位置,計算在新的table的位置
if (next == null) // Single node on list 證明只有一個元素
newTable[idx] = e; // 把當(dāng)前的e設(shè)置給新的table
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e; // 當(dāng)前e
int lastIdx = idx; // 在新table的位置
for (HashEntry<K,V> last = next;
last != null;
last = last.next) { // 遍歷鏈表
int k = last.hash & sizeMask; // 確定在新table的位置
if (k != lastIdx) { // 頭結(jié)點和頭結(jié)點的next元素的節(jié)點發(fā)生了變化
lastIdx = k; // 記錄變化位置
lastRun = last; // 記錄變化節(jié)點
}
}
// 以下把鏈表設(shè)置到新table分為兩種情況
// (1) lastRun 和 lastIdx 沒有發(fā)生變化,也就是整個鏈表的每個元素位置和一樣,都沒有發(fā)生變化
// (2) lastRun 和 lastIdx 發(fā)生了變化,記錄變化位置和變化節(jié)點,然后把變化的這個節(jié)點設(shè)置到新table
// ,但是整個鏈表的位置只有變化節(jié)點和它后面關(guān)聯(lián)的節(jié)點是對的
// 下面的這個遍歷就是處理這個問題,遍歷當(dāng)前頭節(jié)點e,找出不等于變化節(jié)點(lastRun)的節(jié)點重新處理
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
// 處理擴(kuò)容時那個添加的節(jié)點
// 計算位置
int nodeIndex = node.hash & sizeMask; // add the new node
// 設(shè)置next節(jié)點,此時已經(jīng)擴(kuò)容完成,要從新table里面去當(dāng)前位置的頭結(jié)點為next節(jié)點
node.setNext(newTable[nodeIndex]);
// 設(shè)置位置
newTable[nodeIndex] = node;
// 新table替換舊的table
table = newTable;
}