java的Hashmap和ConcurrentHashMap底层原理

一、Hashmap

      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执行效率不高。

2) jdk8,数据结构: 数组+链表+红黑树。

      ①  如果未初始化,则初始化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,则进行开始判断是否转换成红黑树:

    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更新并返回旧值。

二、ConcurrentHashMap

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

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

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

   2>如果是链表和红黑树分别操作进行存值,这里的Treebin其实就是代表整个红黑树对象,以为红黑树可能进行旋转,头结点不固定,所以包装成了Treebin对象进行加锁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章