2020-12-07

1.HashMap的底层数据结构?

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

JDK1.7 数组+链表

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

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

class Node implements Map.Entry {
        final int hash;
        final K key;
        V value;
        Node 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)