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];//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  参考公众号:程序员小灰

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

时间: 2024-08-11 09:48:42

HashMap、ConcurrentHashMap、二叉树、红黑树的相关文章

HashMap简述及红黑树

HashMap是由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突存在的:在JDK8后,当链表长度大于阈值(默认为8)时,链表转化为红黑树,以减少搜索时间. 红黑树简述: https://juejin.im/post/5a27c6946fb9a04509096248#comment 概述:二叉查找树存在缺陷,当形态结构变得单一化时,查找几乎变为线性查找,效率大打折扣,因此引入红黑二叉树. 规则:1.节点是红色或黑色.2.根节点是黑色.3.每个叶子节点都是黑色的空节点(N

二叉树 - 红黑树

RBTree.h #include <iostream> template <typename T> class RBTree { public: RBTree(); bool insert(const T&); bool del(const T&); void show() { Mid_Order(root); } private: enum { RED, BLACK }; typedef struct _node { bool color; T data; st

TreeMap红黑树

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判可以通过其本身的自然顺序(natural ordering),也可以通过构造时传入的比较器(Comparator). TreeMap底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度.其具体算法实现参照了<算法导论>. 出于性能原因,TreeMap是非同步

java随笔——HashMap与红黑树

前言: hashmap是一种很常用的数据结构,其使用方便快捷,接下来笔者将给大家深入解析这个数据结构,让大家能在用的时候知其然,也知其所以然. 一.Map 首先,从最基本的讲起,我们先来认识一下map是个什么东西.在我们写程序的时候经常会遇到数据检索等操作,对于几百个数据的小程序而言,数据的存储方式或是检索策略没有太大影响,但对于大数据,效率就会差很远.我们来讨论一下这个问题. 1.线性检索: 线性检索是最为直白的方法,把所有数据都遍历一遍,然后找到你所需要的数据.其对应的数据结构就是数组,链表

TreeMap源码分析之一 —— 排序二叉树、平衡二叉树、红黑树

一.排序二叉树(BST树) 1.排序二叉树的定义 排序二叉树,Binary Sort Tree 排序二叉树要么是一棵空二叉树,要么是具有下列性质的二叉树: (1)若它的左子树不空,则左子树上所有节点的值均小于它的根节点的值: (2)若它的右子树不空,则右子树上所有节点的值均大于它的根节点的值: (3)它的左.右子树也分别为排序二叉树. 按中序遍历排序二叉树可以得到由小到大的有序序列. 比如 2.排序二叉树的插入和删除 二.平衡二叉树(Balanced Binary Tree,AVL树) 三.红黑

二叉树,平衡树,红黑树,B~/B+树汇总

二叉查找树(BST),平衡二叉查找树(AVL),红黑树(RBT),B~/B+树(B-tree).这四种树都具备下面几个优势: (1) 都是动态结构.在删除,插入操作的时候,都不需要彻底重建原始的索引树.最多就是执行一定量的旋转,变色操作来有限的改变树的形态.而这些操作所付出的代价都远远小于重建一棵树.这一优势在<查找结构专题(1):静态查找结构概论 >中讲到过. (2) 查找的时间复杂度大体维持在O(log(N))数量级上.可能有些结构在最差的情况下效率将会下降很快,比如二叉树 1.二叉查找树

数据结构(三):非线性逻辑结构-特殊的二叉树结构:堆、哈夫曼树、二叉搜索树、平衡二叉搜索树、红黑树、线索二叉树

在上一篇数据结构的博文<数据结构(三):非线性逻辑结构-二叉树>中已经对二叉树的概念.遍历等基本的概念和操作进行了介绍.本篇博文主要介绍几个特殊的二叉树,堆.哈夫曼树.二叉搜索树.平衡二叉搜索树.红黑树.线索二叉树,它们在解决实际问题中有着非常重要的应用.本文主要从概念和一些基本操作上进行分类和总结. 一.概念总揽 (1) 堆 堆(heap order)是一种特殊的表,如果将它看做是一颗完全二叉树的层次序列,那么它具有如下的性质:每个节点的值都不大于其孩子的值,或每个节点的值都不小于其孩子的值

Atitit 常见的树形结构 红黑树 &#160;二叉树 &#160;&#160;B树 B+树 &#160;Trie树&#160;attilax理解与总结

Atitit 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树 attilax理解与总结 1.1. 树形结构-- 一对多的关系1 1.2. 树的相关术语: 1 1.3. 常见的树形结构 红黑树  二叉树   B树 B+树  Trie树2 1.4. 满二叉树和完全二叉树..完全二叉树说明深度达到完全了.2 1.5. 属的逻辑表示 树形比奥死,括号表示,文氏图,凹镜法表示3 1.6. 二叉树是数据结构中一种重要的数据结构,也是树表家族最为基础的结构.3 1.6.1. 3.2 平衡二叉

查找二,二叉树查找与2-3树红黑树

BST: 每个节点的键,都大于其左自述中的任意节点的键,而小于有字数的任意结点的键. 部分实现 get(Node x , Key key){ if(x == null) return null; cmp = key.compareTo(x.key); if(cmp<0) retrun get(x.right,key); else if(cmp>0) retrun get(x.left,key); else return x.val; } 2-3树红黑树: 属于平衡查找树,为了希望保持二分查找树

jdk1.8 HashMap 实现 数组+链表/红黑树

转载至 http://www.cnblogs.com/leesf456/p/5242233.html 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也可以使用红黑树进行存储,总之,目标只有一个,那就是在安全和功能性完备的情况下让其速度更快,提升性能.好~下面就开始分析源码. 二.HashMap数据结构 说明:上图很形象的展示了HashMap的数据