Map集合

作者: 哓晓的故事 | 来源:发表于2018-12-13 13:28 被阅读0次

    table的大小必须时2的幂次方,源于方便扩容
    定位方式,通过key的hashcode值通过再散列h=hashcode^h>>>16的值找到对应的链表,然后equal链表中的val是否相等
    hashMap中的红黑树只是TreeNode
    conccurentHashMap中的红黑树是TreeBin包装的,提供了读写锁
    HashMap源码分析
    HashMap图文解释
    JDK1.7 -> 头插法 -> 查找时间复杂度O(n)链表
    JDK1.8 -> 尾插法不会出现死循环和逆序问题 -> 查找时间复杂度O(logn)红黑树

    HashMap

    JDK1.8-JDK1.7 HashMap差异.png

    get

    1. 链表存在
    2. n=tab.length, 使用 first = tab[(n - 1) & hash] 获取hash值在链表中的首个位置
    3. 如果第一个值就匹配hash和key,立即返回
    4. 如果下一个节点存在
      1. 如果节点为红黑树
      2. 否则,循环链表获取与key相等的值
    

    put

    1. 如果表不存在, 初始化表
    2. 如果表存在, 且值hash与表length &后Node not exists,新增Node
    3. 否则, 对比 key是等于第一个node
      1. 如果是, 使用这个node
      2. 否则, 如果是红黑树
      3. 否则
        1. 如果没有下个节点,新增一个节点,若链表长度大于8,做红黑树排序处理
        2. 否则,判断下个节点的key是否等于当前传入的key
    - 将变更的节点移动到Map的last
    - 增加余数
    - 如果表大小大于阀值,扩容表
    

    hash

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    

    key哈希值与key哈希值右移16位,做异或,可以减少冲突量
    JDK1.7 -> 9次扰动处理=4次位运算+5次异或
    JDK1.8 -> 2次扰动处理=1次位运算+1次异或

    resize

    1. 获取oldTab的length(不存在为0)
    2. 如果存在oldCap>0
        1. 超过最大容量, 设置全局阀值(threshold)为int的最大值
        2. 将oldCap左移一位(扩大一倍), 且旧容量超过初始默认容量, 将threshold左移一位(扩大一倍)
    3. 否则, 如果存在oldThr
        1. 设置newCap为oldThr
    4.  否则, 使用初始化值 cap=1<<4, threshold=loadfactory(=0.75)*cap
    5. 如果不存在 oldTab, 返回newTab
    6. 否则, 遍历旧的 oldTab, 如果存储的node存在
        1. 如果 next 没有指向下一个 node, 则赋予新表->newTab[e.hash & (newCap - 1)] = e;
        2. 否则, 如果 node 为TreeNode(红黑树) -> TODO
        3. 否则, 采用原始位置加原数组长度的方法计算得到位置`hash & oldCap` -> 用到了oldCap来计算key的hash值是否超过
    
    // (e.hash & oldCap) 得到的是 元素的在数组中的位置是否需要移动,示例如下(jdk1.8则是用旧容量&来判断,如果为0说明无需移位,否则需要移位)
    // (e.hash & (oldCap-1)) 得到的是下标位置(jdk1.7每次resize的位置都是与newCap-1重新计算得出)
    // 以上两种方式可以获取新的数值下标,并且重新随机分布,而不会出现死循环问题
    // 这是 JDK1.7 和 JDK1.8的区别
    

    ConcurrentHashMap

    JDK1.7
    JDK1.8.png

    get

    1. 根据key算出spread()[等于hash()]
    2. n=tab.length, 使用 first = tab[(n - 1) & hash] 获取hash值在链表中的Node链表
    3. 如果第一个值就匹配hash和key,立即返回
    4. 否则,循环Node链表获取与key相等的值
      1. 如果是红黑树
      2. 如果当前线程存在(写锁 或者 有waiter),使用链表查询
      3. 否则,读锁+1,最后读锁-1,且是最后一个读锁,释放写线程阻塞

    put

    1. 根据key算出spread()[等于hash()]
    2. 如果table不存在,初始化
      1. 使用sizeCtl来做自旋锁控制,做CAS
      2. sc = n - (n >>> 2); // sizeCtl 设置为0.75*n
    3. 否则,如果table存在,但是hash没有命中,casTabAt新增节点
      1. table是volatile修饰的,但不能保证线程每次都拿到table中的最新元素
      2. Unsafe.getObjectVolatile可以直接获取指定内存的数据
    4. 否则,如果为MOVED,说明hash 正在扩容搬移数据中
      if ((fh = f.hash) == MOVED),执行helpeTransfer 协助扩容
    5. 否则,锁住整个Node链表头(这已经解决了写写锁问题)
      1. 如果f.hash >= 0,说明f是链表结构的头结点,遍历链表设置key
      2. 如果是红黑树,使用红黑树key进行putTreeVal
        1. 查找是否存在key, 存在则覆盖val,否则新建treeBin
        2. 在做红黑树balanceInsertion() -> 参考 Map里面的旋转前后要做加锁解锁
            1. 无写锁 -> +写锁 -> 加锁失败往下走
            /// 下方是一个死循环,直到1才能结束
            1. 如果有写锁,无读锁 -> +读锁 -> 加锁成功设置waiter为null
            2. 否则,如果有写锁,有读锁,无waiter -> +waiter
            3. 否则,如果有写锁,有读锁,有waiter -> 阻塞自身,等待写锁释放
          
      3. 如果节点数超过8,将其转为红黑树
        addCount() 增加计数器,如果sizeCtl达到扩容要求,扩容

    remove

    在删除过程中,需要加sync锁
    如果链表删除要注意,如果删除首节点非首节点要特殊处理
    如果是红黑树,删除后,要重新对红黑树队列做调整(删除过程中要加锁解锁,参考put的)

    1. 先判断是否时首节点,做prev和next处理后
    2. 如果树节点较少,返回true用链表处理
    3. 否则,返回false,说明已经红黑树处理完成了
    4. 找到红黑数最左边的节点,然后交换节点的颜色
    5. 
    

    resizeStamp

    Integer.numberOfLeadingZeros -> 计算一个int型值二进制中第一个1前面0个数
    将RESIZE_STAMP_BITS数值置为1
    保证返回的数值变为负数

    tryPresize 扩容

    1. 构建一个nextTable,大小为table的两倍[这个过程只能一个线程]
    2. 把table的数据复制到nextTable中[采用辅助多线程来进行复制]

    addCount 增加修改次数,选择扩容

    // 此方法设置 sizectl 为负数, 标识正在扩容
    U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2)
    // 此方法标识协助扩容,线程个数数量+1
    U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)
    

    多个线程辅助转移数据,会导致冲突频繁,因此设置了一个步长,相当于不同的线程转移table 中不同 位置段 上的数据,这样可以减小冲突

    transfer

    为了方便并发扩容,减少线程之间的冲突,设置了步长stride,每个线程转移table中位置不同的数据
    扩容拷贝, 和HashMap的resize扩容时的原理类似,加上并发控制

    transfer.png

    size

    [JDK1.7]

    1. 最多计算3次,前2次计算结果做比对(使用modCount作为是否变更数据的计数器)
    2. 如果前2次结果相同,则总数准确,退出
    3. 否则,结果不准确,对segment加锁后再计算一次
    4. 使用segment,锁粒度粗

    [JDK1.8]
    提前到变更时做CAS

    1. 获取基本计数器 baseCount 使用 volatile(不考虑并发的计数器)
    2. 对实时计数器值做过counterCells中的变更value累加
    3. 去除segment,但是每次更新操作,进行计数

    baseCountcounterCells时在变更动作后调用addCount()来记录
    如果不存在counterCells,使用fullAddCount()初始化
    循环 cellValuebaseCountcellBusy来加锁,初始化cellValue值

    foreach

    遍历迭代期器具有 弱一致性,允许修改
    在删除节点时,使用e = e.next的方式,这时候删除的节点还是右指向下一个节点

    相关文章

      网友评论

          本文标题:Map集合

          本文链接:https://www.haomeiwen.com/subject/bpgvhqtx.html