美文网首页
2020-12-07

2020-12-07

作者: 范er_er | 来源:发表于2020-12-07 18:31 被阅读0次

    1.HashMap的底层数据结构?

    数组长度是有限的,我们通过key.hashcode()得到的值有可能是相同的,则会形成链表

    JDK1.7 数组+链表

    public void node(){
        final K key;
        V value;
        final int hash;
         Entry<K,V> entry;
    }
    

    ​ JDK1.8 数组+链表+红黑树

    class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
            }
    

    2.HashMap的存取原理与HashMap是怎么处理hash碰撞的?

    • 存数据
      • 首先会对key进行hashcode,获取它的bucket位置,然后会entry对象放入,如果存在key.hashcode相同[hash碰撞]

    JDK1.7 entry链表插入方式 每次扩容的时候会将所有的entry值重新rehash 会出现环形链表,出现死循环

    java8之前是头插法,就是说新来的值会取代原有的值,原有的值就顺推到链表中去,就像上面的例子一样,因为写这个代码的作者认为后来的值被查找的可能性更大一点,提升查找的效率。
    

    JDK1.8 Node尾部插入了 在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

    • 取数据
      • 逆向,会将key进行hashcode,获取bucket位置,由于entry存放的是个链表,则会进行equal与hashcode比较内存地址

    3.hashmap为啥会线程不安全?有什么线程安全的类代替么?

    • 为啥不安全
      • put与get都未进行加锁,多线程会导致put的值 get获取不同的值;
    • 线程安全的类
      • concurrentHashMap与hashTable

    4.默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?

    • 初始化大小16
    • 1<<4就是16
    • 在使用2的冥的时候,length-1的值所有的二进制的值都为1,index的结果与hashcode的后几位相同,实现了均匀分布
      • index = hashcode(key) & (length -1)

    5.HashMap的扩容方式?负载因子是多少?为什是这么多?

    • 数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容,也就是resize。

      • resize有两个因素

        • Capacity:HashMap当前长度。
        • loadFactor: 负载因子,默认值0.75
        就比如当前的容量大小为100,当你存进第76个的时候,判断发现需要进行resize了,那就进行扩容,但是HashMap的扩容也不是简单的扩大点容量这么简单的。
        
    • 扩容方式

      • 1.创建一个新的 Entry空数组,长度是原数组的2倍
      • 2.Rehash:遍历原Entry 数组,把所有的Entry重新Hash到数组
    • 为啥是loadFactor是0.75

      • 空间换时间 - 如果大于0.75 数组被填满的元素就多,冲突的几率大,链表存的元素就越多,查询不方便
      • 时间换空间-小于0.75,数组被填满的元素少,冲突的几率少,链表存的元素少,查询快

    6.HashMap的主要参数都有哪些?

    • 初始化容量

    • 最大容量

    • 默认负载因子

    • 树形化阈值

    • 解树形化阈值 其实就是当红黑树的节点的个数小于等于6时,会将红黑树结构转为链表结构。

    • 树形化的最小容量

    • 转为红黑树有两个条件

      • 链表的长度大于8
      • HashMap数组的容量大于等于64
      根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
      

    hashTable

    • 1.效率低的原因

      • 对数据操作都会上锁
    • 2.与hashmap不同点

      • 1.hashtable不允许key与value为空值,hashMap是允许的
        • Hashtable在我们put 空值的时候会直接抛空指针异常,但是HashMap却做了特殊处理。
        • Hashtable使用的是安全失败机制(fail-safe)
      • 2.实现方式不同,hashTable继承Dictionary; hashMap继承AbstractMap
      • 3.初始化容量不同,hashTable 是 11 ,hashMap为16
      • 4.扩容机制不同 hashMap是翻倍 hashtable是翻倍+1
      • 5.迭代器不同 hashMap的迭代器iterator 是fail-safe
    • fail-safe

      • 原理

        • 迭代器遍历访问内容,会存在modCount 变量,如果遍历发生内容的改变,则会改变modCount的值,当下次hashNext()/next()遍历下一个节点,会检测 expectedmodCount值,是的话返回遍历,否则抛出异常
      • 场景

        • 并发修改

    ConcurrentHashMap

    • 数据结构是什么,以及为啥他并发度这么高

      1.7 segment数组 +hashEntry组成

      • 数据结构

        • volatile修饰了HashEntry

          1. 可见性-保证了不同线程对这个变量的可见性,一个线程修改值,其他线程可获取修改的值
          2. 排序性-禁止指令重排序
          3. 原子性-只能保证对单次读写的原子性,i++ 不能保证
      • 并发度高的原因

        • 1.采用分段锁 segment 继承的 ReentrantLock

        • 2.存取

        • 1.尝试自旋获取锁

          2.如果重试次数达到 MAX_SCAN_RETRIES 则改为阻塞锁获取

        • 因为用volatile修饰,保存了内存的可见性,获取的都是最新的值

      1.8 抛弃了segment 分段锁,采用 CAS + synchronized 来保证并发的安全性 + node 组成

      cas-乐观锁,读取时不进行加锁,写回时会跟原有值判断是否被修改,如果修改则重新读取

      cas会出现ABA问题

    • 根据 key 计算出 hashcode
    • 判读是否进行初始化
    • 根据key定位出node位置,如果为空,则表示当前可以写入,利用cas尝试写入,失败则自旋保证成功
    • 如果 hashcode == moved == -1 进行扩容
    • 都不满足则用 synchronized 写入
    • 数量大于 链表 8 则会转为红黑树
    • 与hashMap相同
    • key经过hashcode 获取桶的位置
    • 如果是红黑树就按照树的方式获取
    • 如果是链表就按照链表获取

    相关文章

      网友评论

          本文标题:2020-12-07

          本文链接:https://www.haomeiwen.com/subject/jkvtgktx.html