HashMap底层原理

HashMap实现Map接口,非线程安全的,区别于ConcurrentHashMap。允许使用null值和null键,不保证映射的顺序.底层数据结构是一个“数组+链表+红黑树“
put():

  1. 根据key计算得到key.hash = (h = k.hashCode()) ^ (h >>> 16);
  2. 根据key.hash计算得到桶数组的索引index = key.hash & (table.length - 1),这样就找到该key的存放位置了:
    ① 如果该位置没有数据,用该数据新生成一个节点保存新数据,返回null;
    ② 如果该位置有数据是一个红黑树,那么执行相应的插入 / 更新操作
    ③ 如果该位置有数据是一个链表,分两种情况一是该链表没有这个节点,另一个是该链表上有这个节点,注意这里判断的依据是key.hash是否一样: 如果该链表没有这个节点,那么采用尾插法新增节点保存新数据,返回null; 如果该链表已经有这个节点了,那么找到該节点并更新新数据,返回老数据。
    3.更新size和阈值
    扩容机制
    扩容时机:
    1)当链表长度大于8的时候并且数组的长度小于64时优先进行扩容
    2) 当元素个数大于阈值时,进行扩容。
    怎么扩容:
    调用resize()方法方法,
    1.首先进行异常情况的判断,如是否需要初始化,二是若当前容量>最大值则不扩容,
    2.然后根据新容量(是就容量的2倍、)新建数组,将旧数组上的数据(键值对)转移到新的数组中,这里包括:(遍历旧数组的每个元素,重新计算每个数据在数组中的存放位置。(原位置或者原位置+旧容量),将旧数组上的每个数据逐个转移到新数组中,这里采用的是尾插法。)
    3.新数组table引用到HashMap的table属性上
    4.最后重新设置扩容阙值,此时哈希表table=扩容后(2倍)&转移了旧数据的新table

ConcurrentHashMap的底层原理

ConcurrentHashMap的JDK8已经摒弃了Segment段锁的概念,由于在于JDK8的锁粒度更细,理想情况下talbe数组元素的大小就是其支持并发的最大个数,数据结构采用volatile修饰的table数组+单向链表+红黑树的结构,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。
get()

    1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
    2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
    3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
      put(): 对当前的table进行无条件自循环直到put成功
    4. 如果没有初始化就先调用initTable()方法来进行初始化过程
    5. 如果没有hash冲突就直接CAS插入
    6. 如果还在进行扩容操作就先进行扩容(ForwardingNode的hash值判断)
    7. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
    8. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环(树化)
    9. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容。
      扩容机制:
      扩容时机:
      1) 当链表长度大于8的时候并且数组的长度小于64时优先进行扩容
      2) 当数组元素个数大于阈值时,会触发transfer方法,重新调整节点的位置,进行扩容。
      怎么扩容:
      先来看一下单线程是如何完成的:
      它的大体思想就是遍历、复制的过程。首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素:
      如果这个位置为空,就在原table中的i位置放入forwardNode节点,这个也是触发并发扩容的关键点;
      如果这个位置是Node节点(fh>=0),如果它是一个链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上
      如果这个位置是TreeBin节点(fh<0),也做一个反序处理,并且判断是否需要untreefi,把处理的结果分别放在nextTable的i和i+n的位置上
      遍历过所有的节点以后就完成了复制工作,这时让nextTable作为新的table,并且更新sizeCtl为新容量的0.75倍 ,完成扩容。
      多线程是如何完成的:
      如果遍历到的节点是forward节点,就向后继续遍历,再加上给节点上锁的机制,就完成了多线程的控制。多线程遍历节点,处理了一个节点,就把对应点的值set为forward,另一个线程看到forward,就向后遍历。这样交叉就完成了复制工作。
      size():
      1) JDK 8 推荐使用mappingCount 方法(另外的叫size方法),因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值
      2)在没有并发的情况下,使用一个名为 baseCount 的volatile 变量就足够了,当并发的时候,CAS 修改 baseCount 失败后,就会使用 会创建一个CounterCell对象,通常对象的 volatile value 属性是 1。在计算 size 的时候,会将 baseCount 和 CounterCell 数组中的元素的 value 累加,得到总的大小,但这个数字仍旧可能是不准确的。
      3) 还有一个需要注意的地方就是,这个 CounterCell 类使用了 @sun.misc.Contended 注解标识,这个注解是防止伪共享的。是 1.8 新增的。使用时,需要加上 -XX:-RestrictContended 参数。
      size()/mappingCount()–>sumCount(){使用了baseCount变量和CounterCell数组},在put的时候调用了 addCount()方法。
posted @ 2017-10-19 02:53  yuer629  阅读(159)  评论(0编辑  收藏  举报