Map详解

Map详解

1、HashMap

1.1、JDK1.7中的HashMap
1.1.1、底层结构

​ HsahMap在JDK1.7中底层是由数组+链表实现的,数组又被分为一个一个的(bucket),数据通过哈希值确定键值对在这个数组中存放的位置。当哈希值相同时,会以链表的方式存储。每一个键值对会用一个Entry实例对象进行封装,Entry对象里面包含四个属性:key、value、hash值、用于单向链表的指针next。

在这里插入图片描述

Entry对象

在这里插入图片描述

HashMap初始化
  • hashMap初始化时要传递两个参数initialCapacity(初始化容量),loadFactor(负载因子)
    在这里插入图片描述

  • 当用户调用HashMap中无参的构造方法时,初始化容量为:16负载因子为:0.75

//HashMap的无参构造方法
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

//定义的初始化容量常量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//定义的初始化负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
HashMap添加数据的过程(put方法)
//初始化数组
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    int capacity = roundUpToPowerOf2(toSize);

    //计算出扩容阈值
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //初始化数组
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
}

//HsahMap中的put方法
public V put(K key, V value) {
    //当第一个插入数据时,初始化数组和计算出扩容阈值
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    //如果key==null,调用添加存储键为空的方法进行存储
    if (key == null)
        return putForNullKey(value);
    //通过key计算出hash值
    int hash = hash(key);
    //计算出数据存储在数组的索引,计算方法为:h & (length-1)
    int i = indexFor(hash, table.length);
    //如果位置上已经存在该key,那么修改此key的值
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    
    modCount++;
    //添加数据
    addEntry(hash, key, value, i);
    return null;
}

//HashMap添加key为null的方法
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}
  1. 在插入第一个元素到HashMap时数组初始化,先确定数组的大小,然后计算出扩容的阈值。(公式为:数组容量x扩容因子);

  2. 如果key为null,则调用存储key==null的方法,如果key不为空,使用key先计算出Hash值,然后通过(n-1)&hash判断当前元素存放的位置(n为数组的长度),用于判断该元素存放在哪一个Bucket中;

  3. 找到Bucket后,如果当前位置存在元素的话,要判断该位置的元素与要存入的元素的hash值以及key是否相同,如果没有重复,则将此Entry放入链表的头部;如果出现重复,则将此Entry放入链表的尾部,同时建立与前一个节点的连接;

  4. 在插入新值时,如果当前Bucket数组的大小到了阈值,则触发扩容。扩容后,为原来大小的2倍,扩容时会产生一个新数组替换原来的数组,并将原来数组中的值迁移到新数组中

1.1.2、扩容原理
执行过程
  1. 当调用HashMap中的put方法时,内部会调用addEntry方法添加元素;
    在这里插入图片描述
  2. addEntry方法首先会判断是否需要扩容,如果满足扩容条件,则调用内部的resize方法进行扩容,扩容为原来的2倍;

在这里插入图片描述

  1. resize方法中,创建一个新数组,通过transfer方法将以前数组中的Entry迁移到新数组中;

在这里插入图片描述

  1. transfer方法中会循环遍历原数组的Entry,并且重新计算Entry在新数组中的位置,并通过链表的方式连接;
    在这里插入图片描述

  2. 当执行完毕,将table变为新数组;
    在这里插入图片描述

图解扩容
  1. 假设HashMap原始数组大小为2,有三个元素,位置计算公式为:key%数组长度
    在这里插入图片描述

  2. 现在数组进行扩容,扩容为原来的2倍;

在这里插入图片描述

  1. transfer方法为新数组添加元素;
    在这里插入图片描述

  2. 迁移第一个key=3的元素,根据公式计算出在新数组的位置3%4=3,插入完成,指针后移;

在这里插入图片描述

  1. 迁移第二个key=7的元素,根据公式计算出在新数组的位置7%4=3,插入完成,指针后移;
    在这里插入图片描述

  2. 迁移第二个key=5的元素,根据公式计算出在新数组的位置5%4=3,插入完成,遍历结束;
    在这里插入图片描述

1.1.3、死循环解析

在这里插入图片描述

1.2、JDK1.8中的HashMap
1.2.1、底层结构

​ JDK1.8对HashMap进行了存储结构的优化,底层由数组+链表+红黑树组成。加快了数据查询的速度;

​ 在JDK1.8中,如果链表的元素大于等于8个,那么链表会转化为红黑树(注意:前提是桶的大小达到64,否则会先对桶进行扩容);当红黑树中的元素小于等于6个,则红黑树会自动转化为链表

在这里插入图片描述

在这里插入图片描述

1.2.2、源码解析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断数组是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        //为空,对数组扩容
        n = (tab = resize()).length;
    //判断获取到的要插入的位置是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        //为空,直接插入数据
        tab[i] = newNode(hash, key, value, null);
    else {//不为空
        Node<K,V> e; K k;
        //判断要插入元素的key是否已经存在
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            //存在,直接替换值
            e = p;
        //不存在,判断数组该位置是否为红黑树
        else if (p instanceof TreeNode)
            //是红黑树,插入节点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //不是红黑树,遍历链表
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //采用尾插法将数据插入到链表中
                    p.next = newNode(hash, key, value, null);
                    //判断链表长度是否大于等于8
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //将链表转化为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //检验数组是否扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap中put执行图

在这里插入图片描述

2、ConcurrentHashMap

​ ConcurrentHashMap是一个线程安全且高效的HashMap。在并发下,推荐使用其替换HashMap。对于它的使用也非常的简单,除了提供了线程安全的get和put之外,它还提供了一个非常有用的方法putIfAbsent,如果传入的键值对已经存在,则返回存在的value,不进行替换; 如果不存在,则添加键值对,返回null。

2.1、JDK1.7中的ConcurrentHashMap
2.1.1、底层实现

在这里插入图片描述

一个ConCurrentHashMap里包含一个Segment数组,一个Segment中又包含一个HashEntry数组,每个HashEntry就是链表的元素。

ConcurrentHashMap是如何保证线程安全的呢?Segment对象继承了ReentrantLock,在ConcurrentHashMap中相当于锁的角色,在多线程操作时,不同线程操作不同的Segment。只要锁住一个Segment,其他的Segment依然可以操作,这样只要保证每个 Segment 是线程安全的,我们就实现了全局的线程安全。

Segment

在这里插入图片描述

构造方法里面要传入HashEntry数组

HashEntry

在这里插入图片描述

ConcurrentHashMap构造

在这里插入图片描述

根据其构造函数可知,map的容量默认为16,负载因子为0.75。这两个都与原HashMap相同,但不同的在于,其多个参数concurrencyLevel(并发级别),通过该参数可以用来确定Segment数组的长度并且不允许扩容,默认为16。

并发度设置过小会带来严重的锁竞争问题;如果过大,原本位于一个segment内的访问会扩散到不同的segment中,导致查询命中率降低,引起性能下降。

get方法

在这里插入图片描述

  1. 根据key计算出对应的segment

  2. 获取segment下的HashEntry数组

  3. 遍历获取每一个HashEntry进行比对。

注意:整个get过程没有加锁,而是通过volatile保证可以拿到最新值。

put方法
  1. 向下调用ensureSegment方法,其内部可以通过cas保证线程安全,让多线程下只有一个线程可以成功。
    在这里插入图片描述

  2. 在put方法中当初始化完Segment后,会调用 方法进行键值对存放。首先会调用tryLock()尝试获取锁,node为null进入到后续流程进行键值对存放;如果没有获取到锁,则调用**scanAndLockForPut()**自旋等待获得锁。
    在这里插入图片描述

  3. 在**scanAndLockForPut()**方法中首先会根据链表进行遍历,如果遍历完毕仍然找不到与key相同的HashEntry,则提前创建一个HashEntry。当tryLock一定次数后仍然无法获得锁,则主动通过lock申请锁。
    在这里插入图片描述

  4. 在获得锁后,segment对链表进行遍历,如果某个 HashEntry 节点具有相同的 key,则更新该 HashEntry 的 value 值,否则新建一个节点将其插入链表头部。如果节点总数超过阈值,则调用rehash()进行扩容。

在这里插入图片描述

2.2、 JDK1.8的ConcurrentHashMap
get方法

在这里插入图片描述

put方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            //如果table为空,初始化table
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            //CAS向Node数组中存值
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            //扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //通过synchronized锁住数组中的元素
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //是链表中的节点
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //存放数据
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            //尾插法
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,value, null);
                                break;
                            }
                        }
                    }
                    //按照树的方式插入值
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                //达到阈值,链表转化为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //map数量加1,检查是否需要扩容
    addCount(1L, binCount);
    return null;
}
2.2.1、与hashTable的区别
  • Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,竞争越激烈效率越低。 更注重安全。
  • ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处 是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。更注重性能。
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值