本文有些長,貼的源碼較多,請各位看官自備花生瓜子啤酒飲料礦泉水小板凳,且聽我慢慢道來。
Java面試都會問集合,集合必問HashMap,CurrentHashMap,后面的套路就肯定會問多線程、線程安全等等,今天就來學習下HashMap,不對,是補習下。
1、HasMap的屬性
先看下HashMap的繼承體系,它繼承自抽象類AbstractMap,實現了Map、Cloneable、Serializable接口,還有較常用的子類LinkedHashMap也實現了Map接口。
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable{...
public abstract class AbstractMap<K,V> implements Map<K,V> {...
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{...
再看看HashMap的成員變量和一些默認值:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認的初始化數組大小,16 static final int MAXIMUM_CAPACITY = 1 << 30; // HashMap的最大長度 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 負載因子的默認值 static final Entry<?,?>[] EMPTY_TABLE = {}; // Entry數組默認為空 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; // Entry數組 transient int size; // map中key-value 鍵值對的數量 int threshold; // 閾值,即table.length 乘 loadFactor final float loadFactor; //負載因子,默認值為 DEFAULT_LOAD_FACTOR = 0.75 transient int modCount; // HashMap結構被修改的次數 static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE; // 閾值的默認值 HashMap.Holder.trasient int hashSeed; // 翻譯過來叫哈希種子,是一個隨機數, //它能夠減小hashCode碰撞的幾率,默認為0,表示不能進行選擇性哈希(我也不知道是啥意思)
所以我們用默認構造方法new 出來的HashMap(),長度默認為16,閾值為12,並且size達到threshold,就會resize為原來的2倍。
再看下HashMap的一些重要的內部類:
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
}
Entry實現了Map的內部接口Entry,它有四個屬性,key、value、Entry、hash,是HashMap內數組每個位置上真正存放元素的數據結構。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> { public Iterator<Map.Entry<K,V>> iterator() { return newEntryIterator(); } public boolean contains(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<K,V> e = (Map.Entry<K,V>) o; Entry<K,V> candidate = getEntry(e.getKey()); return candidate != null && candidate.equals(e); } public boolean remove(Object o) { return removeMapping(o) != null; } public int size() { return size; } public void clear() { HashMap.this.clear(); } }
EntrySet 繼承了AbstractSet,它內部有個迭代器iterator,可以獲取Entry對象,方法contains用來判斷所給的對象是否包含在當前EntrySet中。
2、put、get、resize方法源碼分析
我們知道HashMap,在jdk1.8之前底層用數組+鏈表實現的,jdk1.8改成了數組+鏈表+紅黑樹實現,以避免長鏈表帶來的遍歷效率低問題。
1)jdk1.7下的源碼
1.1)put()方法
public V put(K key, V value) {
if (table == EMPTY_TABLE) { //(1)
inflateTable(threshold); } if (key == null) //(2) return putForNullKey(value); int hash = hash(key); //(3) int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { //(4) V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(hash, key, value, i); //(5) return null; }
private V putForNullKey(V value) { 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; }
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; //長度必須是2的非零冪 return h & (length-1); //table數組的下標計算:hashCode與(table數組長度減一)做與(&)運算 } &運算,即同是1才為1,否則為0 例如:h1=3 h2=20 length=16 h1: 0011 h2: 10100 length-1: 1111 h1(index): 0011 = 3 h2(index): 0100 = 4 這樣運算得出的index就是舍棄了hashCode一部分高位的hash的值
(1)首先判斷數組若為空,則創建一個新的數組;
(2)如果key為null,遍歷table數組,如果找出key=null的位置,將value覆蓋,並返回舊的value,否則調用addEntry()將它保存到table[0]位置;
(3)若key!=null,則計算出hashCode,算出下標 index,遍歷table
(4)若找到hashCode與當前key的hashCode相等,並且key值也相同,那就覆蓋value的值,並且放回oldValue;
(5)若沒滿足(4)中的條件,則調用方法addEntry(...),下面仔細看下這個方法
若indexFor計算出來的下標在數組中不為空並且size達到閾值,則擴容,然后在index位置創建一個Entry,將key-value放進去。
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; // null的hashCode為0 bucketIndex = indexFor(hash, table.length); } createEntry(hash, key, value, bucketIndex); } void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }
1.2)get() 方法
public V get(Object key) { if (key == null) //(1) return getForNullKey(); Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); //(4) } private V getForNullKey() { //(2) if (size == 0) { return null; } for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } final Entry<K,V> getEntry(Object key) { //(3) if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); 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() 方法就比較簡單啦:
(1) 如果key為null,則判斷HashMap中是否有值,若沒有直接返回null;
(2) 若有就遍歷table數組,找到null對應的value並返回;
(3) 若key不為null,則獲取Entry,也就是一個遍歷table數組命中的過程;
(4) 最后獲取Entry的value,並返回。
1.3)resize() 方法
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { //(1) threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; //(2) transfer(newTable, initHashSeedAsNeeded(newCapacity)); //(3) table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
(1)首先將當前對象的一些屬性保存起來,如果當前HashMap的容量達到最大值,那就無法擴容了,將閾值設置為Integer的最大值並結束方法;
(2)否則創建新的Entry數組,長度為newCapacity,在addEntry()方法中,我們知道newCapacity = 2 * table.length;
(3)然后調用transfer()方法,此方法的作用是將當前數組中的Entry轉移到新數組中;
在存入key-value時會調用initHashSeedAsNeeded()方法判斷是否需要rehash,該方法的過程見注釋,好吧,我也不知道為什么這樣處理得出的結果就能 判斷是否需要rehash,后面就是根據rehash重新計算下標,並將key-value存入新的table中。
/** * Transfers all entries from current table to newTable. */ 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; } } } /** * Initialize the hashing mask value. We defer initialization until we really need it. */ final boolean initHashSeedAsNeeded(int capacity) { boolean currentAltHashing = hashSeed != 0; // 當前哈希種子是否為0 boolean useAltHashing = sun.misc.VM.isBooted() && (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD); // 虛擬機是否啟動,當前數組容量是否大於閾值 boolean switching = currentAltHashing ^ useAltHashing; // 做異或運算 if (switching) { hashSeed = useAltHashing ? sun.misc.Hashing.randomHashSeed(this) : 0; // 重置哈希種子 } return switching; // 返回異或運算的結果,作為是否rehash的標准 }
2)jdk1.8下的源碼
jdk1.8中將Entry改為Node節點來實現的,屬性都是一樣的。
2.1)put()方法
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) // 如果數組是null或者數組為空,就調用resize()進行初始化 n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) //(n-1)&hash 算出下表,這個和1.7是一樣的 tab[i] = newNode(hash, key, value, null); // 如果當前計算出來的位置為null,就新建一個節點 else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key ||
(key != null && key.equals(k)))) // 若計算出來的位置上不為null,它和傳入的key相比,hashCode相等並且key也相等 e = p; // 那么將p賦給e else if (p instanceof TreeNode) // 如果p是樹類型 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 則按照紅黑樹的結構存入進去 else { for (int binCount = 0; ; ++binCount) { // 遍歷p,p是鏈表 if ((e = p.next) == null) { // 如果p的下一個節點是尾節點(尾節點.next=null) p.next = newNode(hash, key, value, null); // 在p的后面創建一個節點,存放key/value(尾插法,多線程並發不會形成循環鏈表) if (binCount >= TREEIFY_THRESHOLD - 1) // TREEIFY_THRESHOLD = 8,即當binCount達到7時轉換成紅黑樹數據結構, // 因為binCount是從0開始的,達到7時p鏈表上就有8個節點了,所以是鏈表上達到8個節點時會轉變成紅黑樹。 treeifyBin(tab, hash); // 這里先就不展開了,紅黑樹不會,有時間再研究 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; // 若上面兩個條件都不滿足,此時e = p.next,也就是將p的下一個節點賦給p,進入下一次循環 } } if (e != null) { // existing mapping for key,jdk這段注釋意思是存在key的映射,我的理解是傳入的key在p位置找到它自己的坑被別人占了 V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) // 下面就是將value存入被占的位置,並將舊的value返回 e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; // 修改次數加一 if (++size > threshold) // 若已有的鍵值對數大於閾值,就擴容 resize(); afterNodeInsertion(evict); return null; }
下面盜個圖,嘿嘿,那老哥畫的太好了,圖片來源:http://www.importnew.com/20386.html,我自己又畫了下,加深點印象。
2.2)get()方法
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }
get()方法也沒什么,就是根據key的hashCode算出下標,找到對應位置上key與參數key是否相等,hash是否相等,如果是樹就獲取樹的節點,如果是鏈表就遍歷直到找到為止,找不到就返回null。
2.3)resize()方法
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; // oldCap就是原數組的長度 int oldThr = threshold; //原閾值 int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold 擴容成兩倍 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults,這里表示初始化resize的另一個作用 newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //創建新數組,容量為原數組的兩倍 table = newTab; //將它指向table變量 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { //遍歷原數組 Node<K,V> e; if ((e = oldTab[j]) != null) { //將不為null的j位置的元素指向e節點 oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; //若e是尾節點,或者說e后面沒有節點了,就將e指向新數組的e.hash&(newCap-1)位置 else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); //如果是樹節點,就按紅黑樹處理,這里不展開 else { // preserve order Node<K,V> loHead = null, loTail = null; // 存放新數組中原位置的節點,這里后面展開說 Node<K,V> hiHead = null, hiTail = null; //存放新數組中原位置+原數組長度的節點 Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { //e.hash&oldCap 的值要么是0要么是oldCap ### if (loTail == null) loHead = e; // 第一次進來,先確定頭節點,以后都走else,loHead指向e else loTail.next = e; // 第二次進來時loTail的next指向e(e=e.next), // 注意此時loHead的地址和loTail還是一樣的,所以loHead也指向e, // 也就是說e被掛在了loHead的后面(尾插法,不會形成循環鏈表), // 以此類推,后面遍歷的e都會被掛在loHead的后面。 loTail = e; // loTail指向e,第一次進來時頭和尾在內存中的指向是一樣的都是e, // 第二次進來時,loTail指向了e(e=e.next),這時和loHead.next指向的對象是一樣的, // 所以下一次進來的時候loHead可以找到loTail.next,並將e掛在后面。
// 這段不明白的可以參考:https://blog.csdn.net/u013494765/article/details/77837338 } else { // 和if里面的原理是一樣的 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; // 將loHead節點存到新數組中原下標位置 } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; // 將hiHead節點存到新數組中 [原下標+原數組長度] 的位置 } } } } } return newTab; }
這里爭對 ### 標注的右岸代碼詳細講下:
為什么(e.hash&oldCap) == 0為true或false就能判斷存放的位置是newTab[原下標],還是newTab[原下標+原數組長度],而不用像jdk1.7那樣每次都要rehash?
3、jdk1.7多線程並發形成循環鏈表問題
4、並發訪問HashMap會出現哪些問題,如何解決呢
經過上面分析,我們知道jdk1.8已經不會在多線程下出現循環鏈表問題了,那還會出現哪些問題呢?
如:數據丟失、結果不一致......
解決方案:
(1)HashTable
用synchronized鎖住整個table,效率太低,不好。
(2)Collections.SynchronizedMap()
它是對put等方法用synchronized加鎖的,效率一般是不如ConcurrentHashMap的,用的不多。
(3)ConcurrentHashMap
采用鎖分段,segment,每次對要操作的那部分數據加鎖,並且get()是不用加鎖的,這效率就高多了。具體實現原理,且聽下回分解。
最后:文中若有寫的不對或者不好的地方,請各位看官指出,謝謝。
參考資料:1、https://juejin.im/post/5b551e8df265da0f84562403
2、http://www.importnew.com/20386.html
3、https://blog.csdn.net/u013494765/article/details/77837338#comments