Hashmap结构图

image.png
初始容量: 16 (1 << 4) 加载因子: 0.75f 采用懒加载模式put数据开始初始化数组
image.png
保证数组长度为2的次方

put方法

调用构造方法初始化容量和负载因子,懒加载的形式,put时候初始化table,通过key的hashcode算出hash值,(高十六位和低十六做异或计算),然后再与上数组的长度-1,算出在数组的位置。数组长度是2的幂次方,比如说16-1就是1111,hash值做与运算就是相当于取余,计算机底层运算快并且hash分布的更均匀。如果不是2的幂次方,肯定有些值取不到。
image.pngimage.pngimage.png
如果数组的位置上没有元素直接把新建节点放上去,如果是红黑树,则在红黑树中进行添加,如果是链表,会循环链表的节点,通过equal方法决定添加还是替换。当链表长度达到8并且数组长度达到64的话会转化成红黑树。
最后,判断节点数是否达到阈值,会进行map的扩容,它会判断原数组的长度,如果是大于1<<30,数组的最大长度,就不进行扩容。否则的话会新建一个数组,是原来的两倍大小。如果原来的桶位没有下一个元素,对新的长度重新计算索引值然后放入,如果是链表,循环元素

链表bin 的数量大于 TREEIFY_THRESHOLD(8)
容量小于 MIN_TREEIFY_CAPACITY(64),只会进行简单的扩容。
容量大于 MIN_TREEIFY_CAPACITY(64),则会进行树化改造。

源码解析

put()

  1. final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
  2. boolean evict) {
  3. Node<K,V>[] tab; Node<K,V> p; int n, i;
  4. if ((tab = table) == null || (n = tab.length) == 0)
  5. // 1 如果 table 为空或者长度为 0,即没有元素,则使用 resize()方法扩容
  6. n = (tab = resize()).length;
  7. if ((p = tab[i = (n - 1) & hash]) == null)
  8. // 2 计算插入存储的数组索引 i,如果数组当前位置为空,则不存在 Hash 冲突,可以直 接插入元素。
  9. tab[i] = newNode(hash, key, value, null);
  10. else {
  11. // 3 插入新元素时,如果发生 Hash 冲突,则依次往下判断。
  12. Node<K,V> e; K k;
  13. // 3.1 判断 table[i]的元素的 key 是否与需要插入的 key 相同,
  14. // 若相同则直接用 新的 value 覆盖掉旧的 value,判断元素相等原则是 equals(),
  15. // 所以 key 对象需要重写该方法。
  16. if (p.hash == hash &&
  17. ((k = p.key) == key || (key != null && key.equals(k))))
  18. e = p;
  19. // 3.2 判断需要插入的数据结构是红黑树还是链表,
  20. // 如果是红黑树,则直接在树中 通过 putTreeVal()方法插入/更新键值对。
  21. else if (p instanceof TreeNode)
  22. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
  23. // 3.3 如果是链表,则在链表中插入/更新键值对
  24. else {
  25. // 3.3.1 遍历 table[i],判断 key 是否已经存在,采用 equals
  26. // 对比当前遍历结点的 key 与需要插入数据的 key,如果存在相同,则直接覆盖。
  27. // 3.3.2 遍历完毕后如果没有发现相同的 key,直接在链表尾部插入新的节点 元素。
  28. // 插入完成后判断链表长度是否>TREEIFY_THRESHOLD(8),若是,则把链 表转换为红黑树。
  29. for (int binCount = 0; ; ++binCount) {
  30. if ((e = p.next) == null) {
  31. p.next = newNode(hash, key, value, null);
  32. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
  33. treeifyBin(tab, hash);
  34. break;
  35. }
  36. if (e.hash == hash &&
  37. ((k = e.key) == key || (key != null && key.equals(k))))
  38. break;
  39. p = e;
  40. }
  41. }
  42. // 3.4 如果 e 不为空,证明 key 已经存在,直接用新的 value 覆盖旧的 value 即可。
  43. if (e != null) { // existing mapping for key
  44. V oldValue = e.value;
  45. if (!onlyIfAbsent || oldValue == null)
  46. e.value = value;
  47. afterNodeAccess(e);
  48. return oldValue;
  49. }
  50. }
  51. ++modCount;
  52. // 4 插入成功后,判断实际存在的键值对数量 size>最大容量,如果大于则进行扩容。
  53. if (++size > threshold)
  54. resize();
  55. // 5 插入成功时会调用的方法(默认实现为空)
  56. afterNodeInsertion(evict);
  57. return null;
  58. }

resize

  1. final Node<K,V>[] resize() {
  2. // 1 复制一份扩容前的数组(当前数组)
  3. Node<K,V>[] oldTab = table;
  4. // 2 保存旧的数组长度,阈值
  5. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  6. int oldThr = threshold;
  7. int newCap, newThr = 0;
  8. // 3 判断扩容前的数组容量
  9. if (oldCap > 0) {
  10. // 3.1 若扩容前的数组容量超过最大值,则不再扩容。
  11. if (oldCap >= MAXIMUM_CAPACITY) {
  12. threshold = Integer.MAX_VALUE;
  13. return oldTab;
  14. }
  15. // 3.2 若没有超过最大值,则扩容为原来二倍。
  16. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  17. oldCap >= DEFAULT_INITIAL_CAPACITY)
  18. newThr = oldThr << 1; // double threshold
  19. }
  20. // 4 如果旧容量为 0,并且旧阈值>0,说明之前创建了哈希表但没有添加元素,初始化 容量等于阈值。
  21. else if (oldThr > 0) // initial capacity was placed in threshold
  22. newCap = oldThr;
  23. // 5 如果旧容量、旧阈值都为 0,说明还没创建哈希表,容量为默认值,阈值为容量*加 载因子。
  24. else { // zero initial threshold signifies using defaults
  25. newCap = DEFAULT_INITIAL_CAPACITY;
  26. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  27. }
  28. // 6 如果新的阈值为 0,则用新容量*加载因子 重新计算一次。
  29. if (newThr == 0) {
  30. float ft = (float)newCap * loadFactor;
  31. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  32. (int)ft : Integer.MAX_VALUE);
  33. }
  34. // 7 更新阈值。
  35. threshold = newThr;
  36. // 8 创建新链表数组,容量是原来两倍
  37. @SuppressWarnings({"rawtypes","unchecked"})
  38. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
  39. // 9 接下来是遍历复制。
  40. table = newTab;
  41. if (oldTab != null) {
  42. for (int j = 0; j < oldCap; ++j) {
  43. Node<K,V> e;
  44. if ((e = oldTab[j]) != null) {
  45. // 9.1 旧的桶置为空
  46. oldTab[j] = null;
  47. // 9.2 如果当前桶只有一个元素,则直接赋值给新 table 的对应位置。
  48. if (e.next == null)
  49. newTab[e.hash & (newCap - 1)] = e;
  50. // 9.3 如果当前桶是树形结构,则要把新 table 当前位置桶也变成树结 构。
  51. else if (e instanceof TreeNode)
  52. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  53. // 9.4 保留旧哈希表桶中链表元素的顺序
  54. else { // preserve order
  55. //按原始链表顺序, 过滤出来扩容后位置不变的元素(低位=0),放在一起。
  56. Node<K,V> loHead = null, loTail = null;
  57. //按原始链表顺序, 过滤出来扩容后位置改变到(index+oldCap)的元素(高位=0),放在一起。
  58. Node<K,V> hiHead = null, hiTail = null;
  59. Node<K,V> next;
  60. // 9.4.1 do-while 循环赋值给新哈希表
  61. do {
  62. next = e.next;
  63. // 9.4.2 通过计算(e.hash & oldCap) == 0 分成两条链表, 再将两条链表散列到新数组的不同位置。
  64. if ((e.hash & oldCap) == 0) {
  65. if (loTail == null)
  66. loHead = e;
  67. else
  68. loTail.next = e;
  69. loTail = e;
  70. }
  71. else {
  72. if (hiTail == null)
  73. hiHead = e;
  74. else
  75. hiTail.next = e;
  76. hiTail = e;
  77. }
  78. } while ((e = next) != null);
  79. // 9.4.3 放到原索引位置不变的桶中。
  80. if (loTail != null) {
  81. loTail.next = null;
  82. newTab[j] = loHead;
  83. }
  84. // 9.4.4 放到原索引+oldCap 位置的桶中。
  85. if (hiTail != null) {
  86. hiTail.next = null;
  87. newTab[j + oldCap] = hiHead;
  88. }
  89. }
  90. }
  91. }
  92. }
  93. return newTab;
  94. }

HashMap默认加载因子为什么选择0.75

HashMap有两个参数影响其性能:初始容量和加载因子。容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行扩容、rehash操作(即重建内部数据结构),扩容后的哈希表将具有两倍的原容量。
通常,加载因子需要在时间和空间成本上寻求一种折衷。
负载因子的大小是主要就是决定了HashMap的数据密度的。
加载因子过高,例如为1,就会可能发生碰撞的几率越高,数组中的链表也会越容易长,虽然减少了空间开销,提高了空间利用率,但同时也增加了插入时的比较次数和查询时间成本,性能会下降。
加载因子过低,例如0.5,虽然可以减少查询时间成本,但是空间利用率很低,同时提高了rehash操作的次数,扩容会影响性能,但是查询和插入时比较次数也会少一些,性能也会提高。
所以建议一般在使用HashMap时建议根据预估值设置初始容量,减少扩容操作,尽量给它大一点空间。
提高空间利用率和 减少查询成本的折中,主要是泊松分布,0.75的话碰撞最小;

如何解决Hash碰撞问题

— 首先调用hashcode方法,去查找相关的key,当有冲突时,再调用equals方法

HashMap 在并发时可能出现的问题

如果多个线程同时使用 put 方法添加元素,而且假设正好存在两个 put 的 key 发生了碰撞(根据 hash 值计算的 bucket 一样),那么根据 HashMap 的实现,这两个 key 会添加到数组的同一个位置,这样最终就会发生其中一个线程 put 的数据被覆盖。
如果多个线程同时检测到元素个数超过数组大小 * loadFactor,这样就会发生多个线程同时对 Node 数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给 table,也就是说其他线程的都会丢失,并且各自线程 put 的数据也丢失。

死循环问题

JDK 1.7 扩容采用的是“头插法”,会导致同一索引位置的节点在扩容后顺序反掉
JDK 1.8 之后采用的是“尾插法”,扩容后节点顺序不会反掉,不存在死循环问题。但是会因为1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题(涉及红黑树)