分析HashTable、HashMap、ConcurrentHashMap的结构、初始化及扩容机制

目录

一、前沿

二、HashTable

三、HashMap

Jdk1.7

Jdk1.8

四、ConcurrentHashMap

Jdk1.7

Jdk1.8

五、总结


一、前沿

相信很多同学对HashTable、HashMap以及ConcurrentHashMap的原理和区别总是一知半解,问到的话,可以说上一些,但是深入问的话,那就茫茫然了。

所以,基于这三个方面,我做了一个系统的总结,希望对大家有所帮助。通过本篇文章,大家可以有以下几点收获:

  1. 了解HashTable的结构、初始化、扩容机制;
  2. 了解HashMap在jdk1.7和jdk1.8各自在数据结构、如何初始化、扩容机制的区别;
  3. 了解ConcurrentHashMap在jdk1.7和jdk1.8各自在数据结构、如何初始化、扩容机制的区别;
  4. 了解了HashTable、HashMap以及ConcurrentHashMap的原理和区别之后,在实际应用中如何选择。

首先需要说明下几个概念,

  1. 容量:Entry[]数组的长度,该值可以手动设置,HashTable默认长度为11,HashMap默认长度为16,实际的使用位置,比如Hashtable test = new Hashtable(12);中的12,再如Map<String , String> test1 = new HashMap<>(2);中的2。
  2. 负载因子:就是一个比例,该值可以手动设置,默认值为0.75。
  3. 阈值:阈值=容量*负载因子,该值通常用来与所有元素(单个(key,value)算一个元素,下同)个数进行对比,用来作为扩容的主要条件。
  4. Entry结构:单个元素的结构:key+value+hash+next,其中hash是对key进行hash的结果,用来确定元素所在位置(即Entry这个桶在Entry[]数组中的位置)使用的,hash和key联合使用来判断该key是否存在,而next用于实现链表结构。

二、HashTable

  1. 线程安全(put和get都是用了synchronized独占锁),键/值均不能为null;

      2.结构:数组+链表

      3.初始化

        1)无参数:容量(数组长度)默认为11,负载因子默认为0.75,阈值为11*0.75=8; 

        2)有参数:根据参数确定容量、负载因子、阈值等。

      4. 扩容机制

        1)扩容条件,当hashtable中所有元素的个数>=阈值,则进行扩容。

        2)扩容过程,伴随着rehash、数据迁移,所以极耗性能,所以在实际使用前,须根据需要,合理设置容量,防止使用过程中发生扩容情况。

        3)容量的上限为Integer.MAX_VALUE - 8,即2147483639

        4)扩容结果,新容量=旧容量*2 + 1,新阈值=新容量*负载因子。

三、HashMap

Jdk1.7

  1. 线程不安全,键/值均可为null;

      2.结构:数组+链表

     3.初始化

        1)无参数:第一次初始化之后,内部数组为空数组,在第一次input是才会对容量等进行初始化,默认容量为16,负载因子为0.75,阈值为16*0.75=12。

        2)有参数:根据参数确定容量、负载因子、阈值等。

     4.扩容机制

        1)扩容条件,元素个数 >= 阈值(threshold),且当前key所在的桶Entry[i]不为null。扩容过程有rehash、复制数据操作,非常消耗性能。

        2)扩容结果,新容量=旧容量*2,新阈值=新容量*负载因子。

        3)容量上限,必须小于 1<<30(1左移30位,代表2的30次方),即1073741824,如果超出这个数,则不再增长,且阈值被设置为Integer.MAX_VALUE,并返回该值,注意Integer.MAX_VALUE仅代表元素个数达到了1073741824,不能在再扩容了,且不能再存储值了。

        4)迁移方法,以头插法进行数据迁移。

        5)Hashmap如何形成环形链表,一定是多线程同时出发resize扩容时,因头插法的机制,会造成一个桶内中的两个元素,他们的next是彼此,这样就会形成一个环形链,进而导致后续遍历数据时出现死循环,导致cpu使用过高的情况。详情参考:HashMap中是如何形成环形链表的?_独行侠的守望の博客-CSDN博客_hashmap环形链表

Jdk1.8

  1. 线程不安全,键/值均可为null;

     2.结构:数组+链表+红黑树

      3.初始化

         1)无参数:实例化的HashMap,默认内部数组为null,即没有实例化,第一次调用input时才初始化,默认容量为16,默认负载因子为0.75,阈值=16*0.75=12。

        2)有参数:如果用户设置容量为固定值n,则hashmap会计算比n大的第一个2的幂数作为初始容量,扩容是也是成倍的扩容,即4变成8,8变成16。

      4.扩容机制

        1)扩容条件,元素个数>阈值,则进行扩容。无参数情况下,首次put时,先触发扩容(初始化),然后存入数据,再后就是判断是否需要扩容。不是首次input时,则不再先触发扩容,而是直接存入数据,然后再判断是否需要扩容。

        2)扩容结果,无论有参数还是无参数初始化,扩容后,新容量=旧容量*2,新阈值=新容量*负载因子。

        3)与红黑树的转换条件,当一个同内的元素>8个时,则该桶内元素有链表转换成红黑树结构;如果当前同的数据结构为红黑树,则当该桶内元素个数<=6个时,会由红黑树转换成链表结构。

        4)数据迁移,扩容时,容量变为原来的2倍,表现为二进制上多了一个高位,高位为0,则新桶的位置不变;如果高位为1,则新桶位置=旧桶位置+旧容量的长度,所以扩容并不需要rehash。Jdk1.8使用尾插法,解决了Hashmap因并发扩容形成环形链表问题。

四、ConcurrentHashMap

Jdk1.7

  1. 线程安全,键值都不能为null,采用了分段锁技术,段segment继承ReentrantLock,每当一个线程占用锁访问一个segment时,其他段不受影响,提升并发。

      2.结构:数组(segment)+数组(HashEntry)+链表(HashEntry节点)

       3.初始化

         1)无参数:

         实例化ConcurrentHashMap,默认内部数组是null,即没有实例化,只是初始化容量(默认为16)、负载因子(默认为0.75)、concurrencyLevel(默认为16),初始化一个长度为16的段的数组,并新建了一个下标为0的segment段,该段中新建了一个长度为2的HashEntry数组,阈值为2*0.75=1;

        在第一次put时,根据key的hash及段相关参数,来确定新的段,然后在该新段下,初始化一个长度为2的HashEntry数组,阈值为2*0.75=1,之后会判断是否扩容,然后再存储值。段的最大值为 1<<16,即65536。

        2)有参数:

        实例化ConcurrentHashMap,根据设置的concurrencyLevel,初始化段长度为不小于concurrencyLevel的最小2的n次方值,且实例化一个下标为0的段,该段中新建了一个长度为2的HashEntry数组,阈值为2*0.75=1。

            在第一次input时,同无参数的情况。

      4.扩容机制

        1)段的大小,segment长度无论使用默认的16,还是指定值,其段的长度在开始初始化之后即确定,不存在扩容一说。

        2)扩容条件,当当前段内ConcurrentHashMap中所有元素的个数>阈值,且HashEntry数组长度<2的30次方(即1073741824)时,进行扩容,扩容伴随数据迁移,非常好耗能。

        3)扩容结果,新容量=旧容量*2,新阈值=新容量*负载因子。每次input,先确定段(没有就新建),然后确定该段内的HashEntry数组是否要扩容,然后再存储值。

Jdk1.8

  1. 线程安全,使用node(或称为table)锁,使用synchronzied(插入数据、数据迁移)+cas(使用unsafe实现,用于并发扩容场景)维护并发,相比jdk1.7减小了锁的粒度。

      2.结构:node数组+链表+红黑树

        3.初始化

        1)无参数:

        实例化ConcurrentHashMap,默认内部数组是null,即没有实例化;

        第一次put时,使用默认容量为16,默认负载因子为0.75,阈值为16*0.75=12,进行初始化一个长度为16的Node[]数组。

        2)有参数:

        实例化ConcurrentHashMap,默认内部数组为null,如果指定concurrencyLevel为n,则cap = n/0.75 +1;

        第一次put时,会创建一个长度不小于cap的最小2的幂数的Node[]数组,负载因子默认为0.75,阈值=容量(Node[]数组长度)*负载因子。

      4.扩容机制

        1)扩容条件,触发扩容有两种方式,第一种,ConcurrentHashMap中元素个数>=阈值,且当前Node[i]桶不为nul,且Node数组长度<2的30次方,即1073741824。第二种,单个桶内链表长度>=8,且Node数组长度<64时,也会触发扩容。

        2)扩容结果,新容量=旧容量*2,新阈值=新容量*负载因子。

        3)转换红黑树结构,单个桶内链表长度>=8,且Node数组长度>=64时,由链表转换成红黑树结构,如果当前桶内元素结构为红黑树,则桶内元素个数<=6时,则由红黑树转换成链表结构。

        4)Node数组上限,为1<<30,即1073741824。

五、总结

到这里,基本接近尾声了,了解了HashTable、HashMap以及ConcurrentHashMap的原理和区别之后,在实际应用中如何选择。

对于单线程小容量数据,HashTable和HashMap区别不大,但是HashTable现在使用的场景很小了。

对于单线程大容量数据,从jdk1.8的hashmap引入红黑树之后,还是选择hashmap吧,毕竟查询效率更高了。

对于多线程,特别是考虑高并发场景,毋庸置疑选择ConcurrentHashMap。

以上就是本片分享的主要内容,如有不对之处,欢迎指正。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值