對於HashMap問題的一些解答
1.當發生沖突時,為什麽把最新值放在鏈表的頭而不是表尾?
答:原因是為了提高效率,放在頭部就不需要遍歷整個鏈表了,放在尾部必須得遍歷整個鏈表再進行插入操作。
2.為什麽哈希表的容量一定要是2的整數次冪?
答:先看一下存儲數據時如何得到數組的索引:
1 static int indexFor(int h, int length) { //根據hash值和數組長度算出索引值 2 return h & (length-1); //這裏不能隨便算取,用hash&(length-1)是有原因的,這樣可以確保算出來的索引是在數組大小範圍內,不會超出 3 }
說一下,這裏h&(length-1)作用其實和hashcode對對數組大小進行取模的值是一樣的,而且取模的效率很低,這樣處理後,不僅使元素均勻散列,還極大的提高了效率。
那麽為什麽哈希表的容量一定要是2的整數次冪呢。首先,length為2的整數次冪的話,h&(length-1)就相當於對length取模,這樣便保證了散列的均勻,同時也提升了效率;其次,length為2的整數次冪的話,為偶數,這樣length-1為奇數,奇數的最後一位是1,這樣便保證了h&(length-1)的最後一位可能為0,也可能為1(這取決於h的值),即與後的結果可能為偶數,也可能為奇數,這樣便可以保證散列的均勻性,而如果length為奇數的話,很明顯length-1為偶數,它的最後一位是0,這樣h&(length-1)的最後一位肯定為0,即只能為偶數,這樣任何hash值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,因此,length取2的整數次冪,是為了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列。相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
3.簡述一下HashMap的實現原理
答:HashMap的底層是用數組加鏈表實現的,HashMap的實現采用了除留余數法形式的哈希函數和鏈地址法解決哈希地址沖突的方案。數組的索引就是對應的哈希地址,存放 的是鏈表的頭結點即插入鏈表中的最後一個元素,鏈表存放的是哈希地址沖突的不同記錄。鏈表的結點設計如下:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next;final int hash; }
next作為引用指向下一個記錄。在HashMap中設計了一個Entry類型的數組用來存放Entry的實例即鏈表結點。
當我們往HashMap中put元素的時候,先根據key的hashCode重新計算hash值,根據hash值得到這個元素在數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麽在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾,數組中存儲的是最後插入的元素 。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。
4.如果兩個鍵的hashcode相同,你如何獲取值對象
HashMap在鏈表中存儲的是鍵值對,找到哈希地址位置之後,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); for (Entry<K,V> e = table[indexFor(hash, table.length)];//根據數組下標返回一個Entry數組,然後循環判斷key的值 e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
5.如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麽辦 ?
HashMap默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的空間的時候,便會進行擴容:
//擴容的方法
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];//創建一個原來兩倍的數組 transfer(newTable, initHashSeedAsNeeded(newCapacity));// 把數組中所以元素再次進行hashcode計算,放入新的數組中 table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
//將當前table中的元素轉移到新的table中
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; 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); e.next = newTable[i]; newTable[i] = e; e = next; } } }
6.為什麽String, Interger這樣的wrapper類適合作為鍵?
String, Interger這樣的wrapper類是final類型的,具有不可變性,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那麽就不能從HashMap中找到你想要的對象。
7.ConcurrentHashMap和Hashtable的區別?
在叠代的過程中,ConcurrentHashMap僅僅鎖定map的某個部分,而Hashtable則會鎖定整個map。所以當Hashtable大小到一定程度時,性能急劇下載。
這裏我想仔細說一下ConcurrentHashMap,因為它很重要!在Java1.7及以前,ConcurrentHashMap使用了鎖分段機制。ConcurrentHashMap有一個Segment內部類,繼承了ReentrantLock:
static final class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1; transient volatile HashEntry<K,V>[] table; transient int count; transient int modCount; transient int threshold; final float loadFactor; Segment(float lf, int threshold, HashEntry<K,V>[] tab) { this.loadFactor = lf; this.threshold = threshold; this.table = tab; } ...... }
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); return s.put(key, hash, value, false); }
從ConcurrentHashMap的put的方法可以看出,它其實是把值存在了Segment類的HashEntry<K,V> 數組裏面了。
ConcurrentHashMap中的HashEntry相對於HashMap中的Entry有一定的差異性:HashEntry中的value以及next都被volatile修飾,這樣在多線程讀寫過程中能夠保持它們的可見性,代碼如下:
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next;
...... }
接下來介紹一下ConcurrentHashMap的rehash:
相對於HashMap的resize,ConcurrentHashMap的rehash原理類似,但是Doug Lea為rehash做了一定的優化,避免讓所有的節點都進行復制操作:由於擴容是基於2的冪指來操作,假設擴容前某HashEntry對應到Segment中數組的index為i,數組的容量為capacity,那麽擴容後該HashEntry對應到新數組中的index只可能為i或者i+capacity,因此大多數HashEntry節點在擴容前後index可以保持不變。基於此,rehash方法中會定位第一個後續所有節點在擴容後index都保持不變的節點,然後將這個節點之前的所有節點重排即可。
private void rehash(HashEntry<K,V> node) { HashEntry<K,V>[] oldTable = table; int oldCapacity = oldTable.length; int newCapacity = oldCapacity << 1; threshold = (int)(newCapacity * loadFactor); HashEntry<K,V>[] newTable = (HashEntry<K,V>[]) new HashEntry[newCapacity]; int sizeMask = newCapacity - 1; for (int i = 0; i < oldCapacity ; i++) { HashEntry<K,V> e = oldTable[i]; if (e != null) { HashEntry<K,V> next = e.next; int idx = e.hash & sizeMask; if (next == null) // Single node on list newTable[idx] = e; else { // Reuse consecutive sequence at same slot HashEntry<K,V> lastRun = e; int lastIdx = idx; for (HashEntry<K,V> last = next; last != null; last = last.next) { int k = last.hash & sizeMask; if (k != lastIdx) { lastIdx = k; lastRun = last; } } 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); } } } } int nodeIndex = node.hash & sizeMask; // add the new node node.setNext(newTable[nodeIndex]); newTable[nodeIndex] = node; table = newTable; }
JDK8:
ConcurrentHashMap在JDK8中進行了巨大改動,它摒棄了Segment(鎖段)的概念,而是啟用了一種全新的方式實現,利用CAS算法。它沿用了與它同時期的HashMap版本的思想,底層依然由“數組”+鏈表+紅黑樹的方式思想。這裏只做一點簡單的介紹,詳細說的話太多了。Java8中的實現也是鎖分離思想,只是鎖住的是一個Node,Node是最核心的內部類,它包裝了key-value鍵值對,所有插入ConcurrentHashMap的數據都包裝在這裏面。
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; ...... }
在ConcurrentHashMap中,隨處可以看到U, 大量使用了U.compareAndSwapXXX的方法,這個方法是利用一個CAS算法實現無鎖化的修改值的操作,他可以大大降低鎖代理的性能消耗。至於紅黑樹,當加入這個節點以後鏈表的長度達到了8並且容量達到64時,就將鏈表轉為紅黑樹
if (binCount >= TREEIFY_THRESHOLD) //TREEIFY_THRESHOLD = 8
treeifyBin(tab, i);
private final void treeifyBin(Node<K,V>[] tab, int index) { Node<K,V> b; int n, sc; if (tab != null) { if ((n = tab.length) < MIN_TREEIFY_CAPACITY) tryPresize(n << 1);// 容量<64,則table兩倍擴容,不轉樹了
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
......
}
8.ConcurrentHashMap已經那麽好了,是不是Hashtable就沒有存在的理由呢?
在叠代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗叠代器的另一種叠代方式,我們稱為弱一致叠代器(在put完後不能立刻得到新的數據)。在這種叠代方式中,當iterator被創建後集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據,iterator完成後再將頭指針替換為新的數據,這樣iterator線程可以使用原來老的數據,而寫線程也可以並發的完成改變,更重要的,這保證了多個線程並發執行的連續性和擴展性,是性能提升的關鍵。然而弱一致性正是它不能取代Hashtable的原因:ConcurrentHashMap的get,iterator 都是弱一致性的。 Doug Lea 也將這個判斷留給用戶自己決定是否使用ConcurrentHashMap。以上就是理由啦。那麽為啥get是弱一致性呢:因為get操作幾乎所有時候都是一個無鎖操作(get中有一個readValueUnderLock調用,不過這句執行到的幾率極小),使得同一個Segment實例上的put和get可以同時進行,這就是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; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { 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; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
9.HashMap使用那種遍歷方式最優?
第一種:
Map map = new HashMap();
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Object key = entry.getKey();
Object val = entry.getValue();
}
效率高,以後一定要使用此種方式!
第二種:
Map map = new HashMap();
Iterator iter = map.keySet().iterator();
while (iter.hasNext()) {
Object key = iter.next();
Object val = map.get(key);
}
可是為什麽第一種比第二種方法效率更高呢?
HashMap這兩種遍歷方法是分別對keyset及entryset來進行遍歷,但是對於keySet其實是遍歷了2次,一次是轉為iterator,一次就從hashmap中取出key所對於的value。而entryset只是遍歷了第一次,它把key和value都放到了entry中,即鍵值對,所以就快了。
參考文章:
http://www.cnblogs.com/ITtangtang/p/3948406.html
http://blog.csdn.net/song19890528/article/details/16891015
http://www.importnew.com/22007.html
https://my.oschina.net/hosee/blog/675423
對於HashMap問題的一些解答