• HashMap、ConcurrentHashMap


    HashMap:

      数组+链表结构。 HashMap是一个用于存储Key-Value键值对的集合,初始化长度16 每次拓展长度必须是2的幂 (为了服务于key映射到index的Hash算法index =  HashCode(Key) &  (Length - 1))。每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干,每个初始值都是null。当进行put操作时,为了使得数据均匀分布会对key进行hash操作再和 HashMap长度进行与运算(Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。)进行与运算来确定Entry的插入位置(index)。对于key进行hash运算得到相同的index时则通过链表来解决 HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可:需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”因为HashMap的发明者认为,后插入的Entry被查找的可能性更大

    使用Get方法根据Key来查找Value的时候,首先会把输入的Key做一次Hash映射,得到对应的index:index =  Hash(“apple”)由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。

    高并发下的hashMap再进行扩容时可能会形成环形链表。解析

    影响发生Resize的因素有两个:

    1.Capacity HashMap的当前长度。上一期曾经说过,HashMap的长度是2的幂。2.LoadFactor HashMap负载因子,默认值为0.75f 衡量HashMap是否进行Resize的条件如下:HashMap.Size   >=  Capacity * LoadFactor

    步骤:

    1.扩容

    创建一个新的Entry空数组,长度是原数组的2倍。

    2.ReHash

    遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。

    让我们回顾一下Hash公式:

    index =  HashCode(Key) &  (Length - 1) 

    当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。

    resize方法:

    void resize(intnewCapacity)
    {
        Entry[] oldTable = table;
        intoldCapacity = oldTable.length;
        ......
        //创建一个新的Hash Table
        Entry[] newTable =new Entry[newCapacity];
        //将Old Hash Table上的数据迁移到New Hash Table上
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }
    transfer方法:
    void transfer(Entry[] newTable)
    {
        Entry[] src = table;
        intnewCapacity = newTable.length;
        //下面这段代码的意思是:
        //  从OldTable里摘一个元素出来,然后放到NewTable中
        for(intj = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if(e != null) {
                src[j] =null;
                do{//链表数据循环
                    Entry<K,V> next = e.next;
                    inti = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];//把entity e 采用头插法放入新HashTable,并且把e.next指向新的table。紧接着把这个节点往下移一个(整个链表关系不变)。则当前节点就在数组上
                    newTable[i] = e;//往下移一个
                    e = next;//链表下一个数据
                }while (e != null);
            }
        }
    }
    单线程下:

     并发下的Rehash:

    假设我们有两个线程。我用红色和浅蓝色标注了一下。

    do{
        Entry<K,V> next = e.next;// <--假设线程一执行到这里就被调度挂起了
        inti = indexFor(e.hash, newCapacity);
        e.next = newTable[i];
        newTable[i] = e;
        e = next;
    }while (e != null);
    

    而我们的线程二执行完成了。于是我们有下面的这个样子。

     
    注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
     

    线程一被调度回来执行。先是执行 newTalbe[i] = e;然后是e = next,导致了e指向了key(7),而下一次循环的next = e.next导致了next指向了key(3)

    线程一继续执行 把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移

    环形链接出现 e.next = newTable[i] 导致 key(3).next 指向了 key(7) 注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

     
     

    ConcurrentHashMap:

    了解之前先了解Segment:Segment本身就相当于一个HashMap对象。同HashMap一样,Segment包含一个HashEntry数组,数组中的每一个HashEntry既是一个键值对,也是一个链表的头节点。

    单一的Segment结构如下:

    像这样的Segment对象,在ConcurrentHashMap集合中有多少个呢?有2的N次方个,共同保存在一个名为segments的数组当中。因此整个ConcurrentHashMap的结构如下:

    可以说,ConcurrentHashMap是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。这样的二级结构,和数据库的水平拆分有些相似

    采用了锁分段技术每一个Segment就好比一个自治区读写操作高度自治,相互之间互不影响

    Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞。由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度,让并发操作效率更高。

    Get方法:

     1.为输入的Key做Hash运算,得到hash值。

     2.通过hash值,定位到对应的Segment对象

     3.再次通过hash值,定位到Segment当中数组的具体位置。

    Put方法:

     1.为输入的Key做Hash运算,得到hash值。

     2.通过hash值,定位到对应的Segment对象

     3.获取可重入锁

     4.再次通过hash值,定位到Segment当中数组的具体位置。

     5.插入或覆盖HashEntry对象。

     6.释放锁。

    Size方法的目的是统计ConcurrentHashMap的总元素数量, 自然需要把各个Segment内部的元素数量汇总起来。但是,如果在统计Segment元素数量的过程中,已统计过的Segment瞬间插入新的元素,这时候该怎么办呢?

    ConcurrentHashMap的Size方法是一个嵌套循环,大体逻辑如下:

    1.遍历所有的Segment。

    2.把Segment的元素数量累加起来。

    3.把Segment的修改次数累加起来。

    4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。

    5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。

    6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。

    7.释放锁,统计结束。

    官方源代码如下:

    public int size() {
    // Try a few times to get accurate count. On failure due to
      // continuous async changes in table, resort to locking.
      final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; // true if size overflows 32 bits
       long sum; // sum of modCounts
       long last = 0L; // previous sum
       int retries = -1; // first iteration isn't retry
       try {
    for (;;) {
    if (retries++ == RETRIES_BEFORE_LOCK) {
    for (int j = 0; j < segments.length; ++j)
    ensureSegment(j).lock(); // force creation
               }
    sum = 0L;
    size = 0;
    overflow = false;
    for (int j = 0; j < segments.length; ++j) {
    Segment<K,V> seg = segmentAt(segments, j);
    if (seg != null) {
    sum += seg.modCount;
    int c = seg.count;
    if (c < 0 || (size += c) < 0)
    overflow = true;
    }
    }
    if (sum == last)
    break;
    last = sum;
    }
    } finally {
    if (retries > RETRIES_BEFORE_LOCK) {
    for (int j = 0; j < segments.length; ++j)
    segmentAt(segments, j).unlock();
    }
    }
    return overflow ? Integer.MAX_VALUE : size;
    }

     为什么这样设计呢?这种思想和乐观锁悲观锁的思想如出一辙。

    为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。

    几点说明:

    1. 这里介绍的ConcurrentHashMap原理和代码,都是基于Java1.7的。在Java8中会有些许差别。

    2.ConcurrentHashMap在对Key求Hash值的时候,为了实现Segment均匀分布,进行了两次Hash。有兴趣的朋友可以研究一下源代码。

    参考链接:https://www.jianshu.com/p/13c650a25ed3  参考公众号:程序员小灰
  • 相关阅读:
    JS倒计时执行操作
    美化radio和checkbox样式
    ajax 多个表单值问题,表单序列化加其它表单值
    .net中的路径问题
    Response.Redirect 打开新窗口的两种方法
    GRIDVIEW FINDCONTROL的使用
    如何验证gridview控件的编辑行?如何获得gridview模板列<ItemTemplate/>中Label值?
    在Repeater的HeaderTemplate和FooterTemplate模板中寻找控件FindControl
    在GridView中使用FindControl(2)
    构造函数(C# 编程指南)
  • 原文地址:https://www.cnblogs.com/leifonlyone/p/12609130.html
Copyright © 2020-2023  润新知