码迷,mamicode.com
首页 > 其他好文 > 详细

HashMap、ConcurrentHashMap、二叉树、红黑树

时间:2020-03-31 23:03:02      阅读:80      评论:0      收藏:0      [点我收藏+]

标签:OLE   hashmap原理   span   try   获取   app   alt   相等   nta   

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];//next指向当前上一个节点,第一次指向新数组时为null
                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  参考公众号:程序员小灰

HashMap、ConcurrentHashMap、二叉树、红黑树

标签:OLE   hashmap原理   span   try   获取   app   alt   相等   nta   

原文地址:https://www.cnblogs.com/leifonlyone/p/12609130.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!