• JDK1.8中HashMap的底层实现


    1.HashMap底层数据结构

    jdk1.7:  数组 + 链表

    jdk1.8: 数组 + 链表 + 红黑树 

    根据源码可知: 数组默认初始长度为16

    链表转为红黑树条件: 数组长度64,链表长度为8

    红黑树退化为链表: 当链表长度为6(避免因在8附近导致链表/红黑树频繁转化) 

    2.头插法与尾插法

    在向hashMap中存数据的过程中,底层实际是将数据存到数组上,经过如下步骤计算: 

    如图: 先拿到key的hashcode(共32位),将前16位与后16位进行异或(^)运算,得到hash值

     再用数组长度-1 和hash 做 位与(&)运算,得到最终数组下标,然后存入.hash函数,也叫扰动函数.

    减少数据存放发生hash冲突的概率.

     当发生hash冲突时,会生成单向链表.插入方式有头插法和尾插法.

    jdk1.7用的是头插法,会存在链表发生反转,多线程环境下可能产生死循环(环形链表)

    jdk1.8改良为尾插法,解决了链表反转和多线程环境死循环问题,但存在多线程环境数据覆盖问题

    3.底层扩容相关机制

    初始数组长度为16,加载因子为0.75(减少hash冲突)

    扩容为原先长度*2,数组长度上线为2的30次幂(位运算是1<<30)

    1.8的条件是> 数组长度* 加载因子 

     如果数组进行扩容,数组长度发生变化,而存储位置 index = h&(length-1),index也可能会发生变化,需要重新计算index。

    HashMap的数组长度一定保持2的次幂,比如16的二进制表示为 10000,那么length-1就是15,二进制为01111,同理扩容后的数组长度为32,二进制表示为100000,length-1为31,二进制表示为011111。从下图可以我们也能看到这样会保证低位全为1,而扩容后只有一位差异,也就是多出了最左位的1,这样在通过 h&(length-1)的时候,只要h对应的最左边的那一个差异位为0,就能保证得到的新的数组索引和老数组索引一致(大大减少了之前已经散列良好的老数组的数据位置重新调换),这也是为啥数组长度必须是2的幂次原因之一。

    另外,数组长度必须是2的幂次的另一原因就是能保证数据分布的均匀性。

    JDK1.8中将transfer()方法的操作也放入了resize()方法中,而由于JDK1.8引入了红黑树的结构,扩容的操作看起来也更加复杂。 

    1. /**
    2. * Initializes or doubles table size. If null, allocates in
    3. * accord with initial capacity target held in field threshold.
    4. * Otherwise, because we are using power-of-two expansion, the
    5. * elements from each bin must either stay at same index, or move
    6. * with a power of two offset in the new table.
    7. *
    8. * @return the table
    9. */
    10. final Node[] resize() {
    11. Node[] oldTab = table;
    12. int oldCap = (oldTab == null) ? 0 : oldTab.length;
    13. int oldThr = threshold;
    14. int newCap, newThr = 0;
    15. if (oldCap > 0) {
    16. if (oldCap >= MAXIMUM_CAPACITY) {
    17. threshold = Integer.MAX_VALUE;
    18. return oldTab;
    19. }
    20. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
    21. oldCap >= DEFAULT_INITIAL_CAPACITY)
    22. newThr = oldThr << 1; // double threshold
    23. }
    24. else if (oldThr > 0) // initial capacity was placed in threshold
    25. newCap = oldThr;
    26. else { // zero initial threshold signifies using defaults
    27. newCap = DEFAULT_INITIAL_CAPACITY;
    28. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    29. }
    30. if (newThr == 0) {
    31. float ft = (float)newCap * loadFactor;
    32. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
    33. (int)ft : Integer.MAX_VALUE);
    34. }
    35. threshold = newThr;
    36. // 新建数组
    37. @SuppressWarnings({"rawtypes","unchecked"})
    38. Node[] newTab = (Node[])new Node[newCap];
    39. table = newTab;
    40. if (oldTab != null) {
    41. // 数据转移操作
    42. for (int j = 0; j < oldCap; ++j) {
    43. Node e;
    44. if ((e = oldTab[j]) != null) {
    45. oldTab[j] = null;
    46. // 元素没有后续节点,直接放入新数组对应索引位置
    47. if (e.next == null)
    48. newTab[e.hash & (newCap - 1)] = e;
    49. // 元素是树节点,进行树转移操作(本文暂不考虑)
    50. else if (e instanceof TreeNode)
    51. ((TreeNode)e).split(this, newTab, j, oldCap);
    52. else { // preserve order
    53. // 不是树节点并且有后续节点那就只剩下链表形式了
    54. Node loHead = null, loTail = null;
    55. Node hiHead = null, hiTail = null;
    56. Node next;
    57. // 尾插法转移链表数据
    58. do {
    59. next = e.next;
    60. // 索引不进行变化,放入新数组和原数组一样的位置
    61. if ((e.hash & oldCap) == 0) {
    62. if (loTail == null)
    63. // 直接放入
    64. loHead = e;
    65. else
    66. // 尾插法
    67. loTail.next = e;
    68. loTail = e;
    69. }
    70. else {
    71. // 需要重新计算元素在新数组中的位置
    72. if (hiTail == null)
    73. // 直接放入
    74. hiHead = e;
    75. else
    76. // 尾插法
    77. hiTail.next = e;
    78. hiTail = e;
    79. }
    80. } while ((e = next) != null);
    81. if (loTail != null) {
    82. loTail.next = null;
    83. newTab[j] = loHead;
    84. }
    85. if (hiTail != null) {
    86. // 重新计算的数组索引位置也就是原索引加上原数组长度
    87. hiTail.next = null;
    88. newTab[j + oldCap] = hiHead;
    89. }
    90. }
    91. }
    92. }
    93. }
    94. return newTab;
    95. }

    4.concurrentHashMap

    上面说到,jdk1.8虽然解决了链表反转和死循环问题,但是依然存在多线程环境数据覆盖的问题.

    使用线程安全的 Hashtable 类代替,该类在对数据操作的时候都会上锁,也就是加上 synchronized

    使用线程安全的 ConcurrentHashMap 类代替,该类在 JDK 1.7 和 JDK 1.8 的底层原理有所不同,JDK 1.7 采用数组 + 链表存储数据,使用分段锁 Segment 保证线程安全;JDK 1.8 采用数组 + 链表/红黑树存储数据,使用 CAS + synchronized 保证线程安全。

    不过**前两者的线程并发度并不高,容易发生大规模阻塞,**所以一般使用的都是 ConcurrentHashMap,他的性能和效率明显高于前者。

    ConcurrentHashMap1.7

     Java 7 中 ConcurrentHashMap 的存储结构如上图,ConcurrnetHashMap 由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,你也可以认为 ConcurrentHashMap 默认支持最多 16 个线程并发。

    JDK1.8 ConcurrentHashMap在性能方面的优化

    1. 在jdk1.8里面ConcurrentHashMap锁的粒度,是数组中的某一个节点,而在jdk1.7里面。它锁定的是Segment,锁的范围要更大,所以性能上它会更低。

    2. 引入红黑树这样一个机制,去降低了数据查询的时间复杂度,红黑树的时间复杂度实是O(logn)

    3. 底层数据结构:Synchronized + CAS +Node +红黑树.Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS

      为什么在有Synchronized 的情况下还要使用CAS?

      因为CAS是乐观锁,在一些场景中(并发不激烈的情况下)它比Synchronized和ReentrentLock的效率要高,当CAS保障不了线程安全的情况下(扩容或者hash冲突的情况下)转成Synchronized 来保证线程安全,大大提高了低并发下的性能.

  • 相关阅读:
    记一次 .NET某账本软件 非托管泄露分析
    CSDN云IDE初体验 - 有些惊艳
    Python中ArcPy按照分幅条带与成像日期拼接每个8天间隔内的遥感影像
    【数据挖掘】2022年京东算法工程师笔试题(23届)
    算法练习- LeetCode 剑指 Offer 56 - I. 数组中数字出现的次数
    C++ 模板和 C# 泛型之间的区别【示例语法说明】
    keycloak~AbstractJsonUserAttributeMapper的作用
    新一代网络请求库:python-httpx库
    零基础HTML入门教程(15)——合并单元格
    使用send给生成器注入数据
  • 原文地址:https://blog.csdn.net/footbridge/article/details/127652044