java的Hashmap和ConcurrentHashMap底层原理

一、Hashmap

      1.1 jdk7,使用数据结构,数组(Entry<K,V>[]  table)+链表

       ① 当hashmap没有初始化时,初始化hash,hashmap由Entry<K,V>[]  table组成,初始化时初始值大小为2的幂次方大小,比如 HashMap hashMap = new HashMap(17,0.75f);想要初始化长度为17,最终会被转化成32(为了后面位运算,方便通过hashcode进行位运算时,获取到key对应的在数组中的位置),根据扩容扩容因子计算一下扩容的阈值。

       ② hashmap是允许key和value为null的,只不过会默认放置到table数组的第0个位置。

       ③ 根据key的hashcode计算应该存放到table[]数组的哪个位置下。

               1> 下图对于hashcode的计算主要是为了降低hashcode的重复率,先降碰撞率。

              2>这里的&操作就是为了计算生成的hashcode在table[]数据的哪个位置,相当与取模运算,length-1,因为length为2的幂次方比如16,用二级制表示为: ... 0001 0000,那么16-1=15,二级制表示: ... 0000 1111,&运算时只有最后四位生效,其余 全是0,并且最后四位的值就是h的值,相当于 h 对 15 取余运算(只对2的幂次方有效,所以会默认生成2的幂次方大小的长度)。

        ③ 判断,如果对应的table[]数组位置不为空,则遍历数组下的链表,直到找到为空或者找到一个完全相同的key。key相同则更新key对应value,如果找不到对应的key,则跳出循环,对hashmap的modCount+1,记录hashmap被修改的次数(快速失败的时候会用到),进行第四步。

         ④ 核心步骤,添加元素,这里也分两步,扩容和添加数据。

     1) 扩容,扩容条件:大于阈值,默认16*0.75=12,存放数据时,对应的数组位置是否为空,扩容为原来的2倍。

          1> new 一个新的tables[]数组,然后进行数据转移。        

            2> 数据转移,其实就是把原来数据重新进行计算下hashcode,然后放入到新的table[]数组中结束。

           

     2) 存数据,其实就是新生成了一个entry,然后取出table[]数组中原来的头结点,放入新的entry中,再放入的到对应的table[]数组中(头插法),并给size +1结束。

     

⑤ Hashmap对比HashTable:

      1)HashTable存储的value不允许为null值。

      2)  HashTable 是线程安全的,因为hashTable为了保证线程安全,把对应的get和put等方法都加上了synchronized关键字来确保线程安全。

     3)  HashTable执行效率不高。

⑥ jdk7 hashmap死锁:并发的HashMap为什么会引起死循环?_hashmap并发死循环原理_bboyzqh的博客-CSDN博客 (地址引用造成3----7----3)的循环引用

1.2 jdk8,数据结构: node数组+链表+红黑树

      ①  如果未初始化,则初始化hashmap,resize()方法也可以进行初始化操作,取出key对应的hashcode进行&运算(与jdk7是一样的思路),计算出对应数组的位置,如果为空这新建一个node。

                 1>区别于jdk7这简化了key的hash算法。

      ② jdk8的数组变成了node<K,V> tab,而jdk7是entry<K,V>[] tables 内部结构基本一样,判断取出来的头node是否和传进来的一样,如果相等则赋值给局部变量e。

     ③ 看取出来的节点是不是TreeNode,如果是代表该数组对应的位置变成了红黑树,进行红黑树的插入。

     ④ 如果取出来的不是TreeNode,则代表当前依然是链表,for循环遍历链表:

       1> 如果遍历到一个节点为null时 (也就是没有key与传进来的值相等时),则创建一个新的节点,如果已有8个node,当前进来的为第9个,即当某一个链表长度大于8,则进行开始判断是否转换成红黑树:

默认情况下,会判断数组长度是否小于64,若小于64则使用resize()进行扩容。反之调用treeifyBin()转红黑树。

    1、判断如果tables[]数组长度不大于64,则将table[]数组扩容。关于槽位移动的思想是这样的:

          将原来的一条链表拆成两条链表,低位链表的数据将会到新数组的当前下标位置(原来下标多少,新下标就是多少),高位链表的数据将会到新数组的当前下标+当前数组长度的位置(原来下标多少,新下标就是多少+当前数组长度)。

          计算新的槽位下标是看当前hash与旧数组长度相与,结果为0的话那么新槽位下标还是当前的下标,如果非零,那么新槽位下标是当前下标+当前数组长度。举个?:hash为1,当前数组长度为8,1&8 为 0,所以下一个槽位就是1;hash为9,当前数组长度为8,9&8 不为 0,所以下一个槽位就是1+8 = 9。

     2、如果大于64,并且table[]数组当前位置不为空时,则先生成一个双向链表,然后在转化成红黑树。

       2> 如果找到一个节点的key与入参key的相同则跳出循环。

      ⑤ 如果局部变量e不为空(有相同key时),将新的value更新并返回旧值。

        还有一点需要留意的是链表转为红黑树的阈值是8,而红黑树退化成链表的阈值是6。为什么这两个值会不一样呢?

可以试想一下,如果这两个值都为8的话,而当前链表的节点数量为7,此时一个新的节点进来了,计算出hash值和这七个节点的hash值相同,即发生了hash冲突。于是就会把这个节点挂在第七个节点的后面,但是此时已经达到了变成红黑树的阈值了(MIN_TREEIFY_CAPACITY条件假定也满足),于是就转成红黑树

二 ConcurrentHashMap

      2.1 jdk7 分段锁Segment<K,V>[]数组 + hashEntry +红黑树 ,通过unsafe方法+cas保证了线程安全+AQS+死循环   

          ①初始化 ConcurrentHashMap<Object, Object> objectObjectConcurrentHashMap = new ConcurrentHashMap<>();

       1> ssize为segment[]数组长度,默认16,同样要是自定义的2的幂次方。segment[]数组下hashEntry长度,同样是2的幂次方,并且要大于,ConcurrentHashMap的长度:initialCapacity/segment[]数组长度。实际的初始化长度最小是2。

      2> 这里会额外初始化一个segment[0]用于后续初始化segment做参考,提高性能,通过unsafe方法操作内存放入到segment[]数组中。

      ② put方法

       1> ConcurrentHashMap 不允许存null值会报错。

       2> 同样根据key的hashcode,取余计算对应的segment[]数组中的位置,不过这里为了保证线程安全,采用的是unsafe方法

       ③ 存值

   1>尝试加锁: 自旋加锁

        while死循环尝试加锁,每尝试一次retires +1,多核尝试超过64次,则变成lock(),阻塞获取锁,这里锁住的都都是segment对象,尝试过程中,根据hash值获取key对应的hashEntry[]数组中的位置,如果节点位置为null则尝试创建。

   2> 尝试遍历  key对应位置的,HashEntry(也是个链表),如果有key值相同时则更新值。

   3> 如果遍历过程中到达尾节点依然为null,则创建新值,存进去(尾插法),如果超过阈值则扩容,扩容方式大概与1.8中的思路相同。

2.2 jdk8 去掉了segment<K,V>[]数组的概念,只是对头结点进行加锁(可能是node,也肯能是treebin),同时引入了红黑树,复杂的是resize()和size()方法

    1>构造方法简化,不在初始化concurrentHashMap,不允许存放null值和null键。

   2>如果是链表和红黑树分别操作进行存值,这里的Treebin其实就是代表整个红黑树对象,因为红黑树可能进行旋转,头结点不固定,所以包装成了Treebin对象进行加锁,加锁方式改成了synchronized,因为优化后的synchronized 关键字加锁效率不算慢。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值