JDK1.7头插法导致的循环
当多线程去扩容HashMap时,因为头插法会产生链表成环的问题:
若当前线程此时获得entry节点,但是被挂起,而另外一个线程已经完成扩容操作,并且因扩容和头插法操作导致链表顺序颠倒。
而原来被挂起的线程继续执行,并且继续才用头插法扩容同一个链表,因为此时链表顺序因为另一个线程的头插法颠倒了,所以当前线程再次采用头插法会将下一个节点的next指向自己。
导致两个节点的next互相引用对方,因此产生环。
HashMap原理
JDK8之前HashMap有数组+链表组成,数组为HashMap主体,链表用来解决Hash冲突。
JDK8及之后在解决哈希冲突的时候发生了变化,在链表长度大于阈值(默认为8),会将链表转换为红黑树(链表转红黑树前会判断,如果当前数组长度小于64,会先进行数组的扩容,而不是转换为红黑树。(该特性参考treeifBin方法
拉链法
将数组和链表结合,数组中的一个元素就是一个链表,若遇到哈希冲突,将冲突的值添加到链表即可。
HashMap类的属性
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {// 序列号private static final long serialVersionUID = 362498820763181265L;// 默认的初始容量是16static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;// 最大容量static final int MAXIMUM_CAPACITY = 1 << 30;// 默认的填充因子static final float DEFAULT_LOAD_FACTOR = 0.75f;// 当桶(bucket)上的结点数大于这个值时会转成红黑树static final int TREEIFY_THRESHOLD = 8;// 当桶(bucket)上的结点数小于这个值时树转链表static final int UNTREEIFY_THRESHOLD = 6;// 桶中结构转化为红黑树对应的table的最小大小static final int MIN_TREEIFY_CAPACITY = 64;// 存储元素的数组,总是2的幂次倍transient Node<k,v>[] table;// 存放具体元素的集transient Set<map.entry<k,v>> entrySet;// 存放元素的个数,注意这个不等于数组的长度。transient int size;// 每次扩容和更改map结构的计数器transient int modCount;// 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容int threshold;// 加载因子final float loadFactor;}
- loadFactor
用于动态计算阈值,也用于表示该map的数据疏密程度,loadFactor过大会导致元素密集,hash碰撞的可能性也就越大,查找的效率也就越低。而loadFactor过小会导致数组利用率低,存放的数据分散。官方建议默认值0.75是一个比较好的临界值。 - threshold
临界值,threshold = capacity * loadFactor当size >= threshold时,就要考虑对数组进行扩容
Node节点
// 继承自 Map.Entry<K,V>static class Node<K,V> implements Map.Entry<K,V> {final int hash;// 哈希值,存放元素到hashmap中时用来与其他元素hash值比较final K key;//键V value;//值// 指向下一个节点Node<K,V> next;Node(int hash, K key, V value, Node<K,V> next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}public final K getKey() { return key; }public final V getValue() { return value; }public final String toString() { return key + "=" + value; }// 重写hashCode()方法public final int hashCode() {return Objects.hashCode(key) ^ Objects.hashCode(value);}public final V setValue(V newValue) {V oldValue = value;value = newValue;return oldValue;}// 重写 equals() 方法public final boolean equals(Object o) {if (o == this)return true;if (o instanceof Map.Entry) {Map.Entry<?,?> e = (Map.Entry<?,?>)o;if (Objects.equals(key, e.getKey()) &&Objects.equals(value, e.getValue()))return true;}return false;}}
HashMap构造方法
// 默认构造函数。public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}// 包含另一个“Map”的构造函数public HashMap(Map<? extends K, ? extends V> m) {this.loadFactor = DEFAULT_LOAD_FACTOR;putMapEntries(m, false);//下面会分析到这个方法}// 指定“容量大小”的构造函数public HashMap(int initialCapacity) {this(initialCapacity, DEFAULT_LOAD_FACTOR);}// 指定“容量大小”和“加载因子”的构造函数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;this.threshold = tableSizeFor(initialCapacity);}
putMapEntries方法
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {// 判断table是否已经初始化if (table == null) { // pre-size// 未初始化,s为m的实际元素个数float ft = ((float)s / loadFactor) + 1.0F;int t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);// 计算得到的t大于阈值,则初始化阈值if (t > threshold)threshold = tableSizeFor(t);}// 已初始化,并且m元素个数大于阈值,进行扩容处理else if (s > threshold)resize();// 将m中的所有元素添加至HashMap中for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}
putMapEntries方法逻辑:
- 先判断加进去的map大小是否为0,大于0则继续执行操作
- 再通过
table==null判断Hashmap类是否初始化- 若未进行初始化,将待加入的map的大小除以loadfactor作为参考大小,将参考大小与Map最大容量做比较,保证参考大小不超过MAXIMUM_CAPACITY作为实际加入map的size。若这个size大于了threshold,进行扩容处理。
- 若进行了初始化,并且待加入的map大小大于阈值threshold,进行resize()处理(一般情况下resize会将原map扩容至两倍)
- 最后遍历待加入的map的Entry,将Entry放入目的map。
Put方法逻辑(JDK1.8)

HashMap只提供了put方法给用户使用,而put方法调用putVal方法,这样对于用户隐藏了一些参数细节。
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)//注意resize的作用为初始化map或double这个map的大小,这里为初始化操作n = (tab = resize()).length;//如果table上的位置为null则直接插入,(n-1)&hash为确定下标的操作if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);//若桶中已经存在元素else {Node<K,V> e; K k;//比较桶中的第一个元素,若hash和key都相等,直接覆盖if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//如果key不相等,且该节点是树节点则调用putTreeValelse if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {//都不是则该节点为链表,这里用一个binCount来表示链表的长度for (int binCount = 0; ; ++binCount) {//采用尾插法,配合下面的 p = e 遍历到尾节点if ((e = p.next) == null) {//插入链表p.next = newNode(hash, key, value, null);//插入后若链表长度为8,进行树化操作if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}//若此节点的key与待插入节点的key相同则跳出,跳出后再进行覆盖if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;//与前面的e = p.next组合,遍历链表p = e;}}//覆盖操作if (e != null) {//记录旧值V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;//访问后回调afterNodeAccess(e);return oldValue;}}//结构性修改++modCount;//实际大小大于阈值则扩容if (++size > threshold)resize();//插入后回调afterNodeInsertion(evict);return null;}
jdk1.7的put
与JDK1.8不同的地方主要在于,链表树化以及头插法插入链表两点。
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;//一边赋值一边检查table不为null、长度大于0、第一个元素能取到if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {//通过对比key先看看第一个元素是不是要找的元素if (first.hash == hash &&((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;}
resize方法
resize还是非常耗时的,知道未来需要多少容量的情况下建议先一次性给足空间
final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;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)//如果新容量超过了最大的容量,就不计算threshold了,因为也用不到newThr = oldThr << 1; // double threshold}//capacity没初始化就初始化capacityelse if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;//用默认大小(16)初始化capacity和thresholdelse { // zero initial threshold signifies using defaultsnewCap = 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"})//根据容量创建新的tableNode<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {//开始遍历for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {//用e存储了oldTab[j],所以这里能设置为nulloldTab[j] = null;//单节点情况下if (e.next == null)//rehash操作newTab[e.hash & (newCap - 1)] = e;//树节点情况下else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // preserve order//这里有两条链表,区分的标准见下面那个if语句Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;//注意这里不是掩码的作用,而是看oldcap那一位是否为零//为零表示新容量不影响原来的Index计算,可以用原来的索引下标//至于为啥只看一位:因为resize是按两倍来扩容的if ((e.hash & oldCap) == 0) {//这里先把头存起来if (loTail == null)loHead = e;//再往下链接elseloTail.next = e;loTail = e;}//为1表示要进行一次偏移,偏移量为oldcapacityelse {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//下面两个if把事先存好的链表头接到tableif (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}
