说一说HashMap和ConcurrentHashMap

109 阅读4分钟

image.png

一、HashMap

1、数据结构

  • java7

    • 数组加链表
  • java8

    • 数组+链表+红黑树

      • 转化树的目的

        • 链表越长查找效率越来越低,所以转成树,耗时log(n),短的时候链表快
      • 何时转化

        • 链表元素大于等于8并且数组长度超过64转为树

          • 链表达到阈值但是数组长度没超过64,发生扩容,先利用扩容来缩小链表的长度
        • 小于等于6还原链表

        • 中间有个差值7可以有效防止链表和树频繁转换

        • 链表转红黑树阈值是8,实际链表长度9

        • 源码注释,泊松分布,8的时候hash冲突概率已经很小了

      • 红黑树

image.png

2、初始容量

  • java7

    • 16

      • 考虑到了效率和内存使用的权衡。这个值不能太大,以避免浪费空间;也不能太小,以免频繁发生扩容影响效率
  • java8

    • 0,put的时候才初始化16

3、扩容

  • 阈值=容量*加载因子0.75。默认12,超过扩容

  • 新容量=旧容量*2

  • 扩容的过程就是对key重新计算hash,然后把数据拷贝到新的数组

  • java7使用头插法会导致环形链表

    是因为并发扩容导致的,例如线程 T1 和T2进行扩容操作,此时 T1 和 T2 指向的是链表的头结点元素 A,而 T1 和 T2 的下一个节点,指向的是 B 节点

image.png

此时线程 T2正好挂起,线程 T1 扩容完成后,线程 T2 才被唤醒

image.png

在头插法中,新加入的节点总是被插入到链表的头部。所以,T1 执行完之后的顺序是 B 到 A,T2继续执行,将A插入完,循环至B, 此时B的next指向A(T1做了全局的修改), B执行完后。循环发现B.next!=null,把A又执行了一次,根据头插法,A的next又指向B.

image.png

当使用get获取元素时,发现A.next=B,B.next=A;形成环状,导致查询出现了死循环

  • java8

尾插法,不会变化原Node的next关系,解决死循环的问题

4、put

  • 通过key的hash值对数组长度取余

    • 取余效率不如位移运算:hash&(length-1)
    • 两者相等的前提是length是2的n次方,所以map容量都是向上取2的倍数
  • 找到数组中的位置之后,如果数组中没有元素直接存入,反之则判断key是否相同,key相同就覆盖,否则就会插入到链表的尾部,如果链表的长度超过8,则会转换成红黑树,最后判断数组长度是否超过默认的长度*负载因子也就是12,超过则进行扩容。

5、get

计算key的hash值,定位数组位置,遍历链表或者红黑树找到相同的key返回value

6、线程不安全

  • java7

    • 在多线程环境下,并发扩容时会造成环形链或数据丢失。
  • java8

    • 在多线程环境下,put会发生数据覆盖的情况。

二、ConcurrentHashMap

1、数据结构

  • java7

    • ReentrantLock+Segment+hashEntry,通过Segment分段锁保证线程安全,segment继承ReentrantLock,默认16个,并发16,每个Segment包含一个HashEntry数组,默认容量2,每个HashEntry又是一个链表
  • java8

    • synchronized+CAS+Node(实现Map.Entry<K,V>)+红黑树,通过synchronized+CAS保证线程安全,锁链表的head节点,锁粒度更细,效率更高

2、put

  • java7

    • 计算key的hash,定位到segment,segment如果是空就先初始化
    • 使用ReentrantLock加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定获取锁成功
    • 二次hash,定位HashEntry数组下标,遍历HashEntry,就是和HashMap一样,数组中key一样就直接替换,不存在就再插入链表,超过扩容,释放锁
  • java8

    • 首先计算hash,遍历node数组,如果当前数组位置是空则直接通过CAS自旋写入数据
    • 否则如果hash==MOVED,说明需要扩容,执行扩容
    • 如果都不满足,就使用synchronized写入数据,key一样就覆盖,反之就尾插法,链表长度超过8就转换成红黑树

3、get

  • java7

    • key通过hash定位到segment,再遍历链表定位到具体的元素上,需要注意的是value是volatile的,所以get是不需要加锁的
  • Java8

    • 同hashmap

4、特性

java8,不允许nullkey和nullvalue,会跟get返回null产生歧义

5、线程安全

利用锁机制保证线程安全