文章标题

HashMap基础知识

–来源:伯乐专栏作者/玻璃猫,微信公众号 - 程序员小灰

简述

HashMap是由一个个键值对的集合,每个键值对也叫Entry,分散存储在一个数组中。
HashMap数组每一个元素的初始值都是Null。
常用操作:Get和Put

Put原理

hashMap.put(“apple”, 0)
利用一个哈希函数来确定Entry的插入位置(index)
index = Hash(“apple”)
HashMap的长度是有限的,难免会出现index冲突的情况,这时利用链表来解决。
每个Entry对象也是一个链表的头节点
每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可
需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。至于为什么不插入链表尾部,后面会有解释。

头插法:
index=2
Entry6.next = table[index]
table[index]=Entry6

![](http://mmbiz.qpic.cn/mmbiz_png/NtO5sialJZGoXt6UNkvyibQ8ufeF48pvm29aBmKLDfHETNia2Lzpuia9tm9IDX5XXue0nGoSgFpUGT0crAS45zICQQ/640?wx_fmt=png&tp=webp&wxfrom=5&wx _lazy=1)

Get方法的原理

index = Hash(“apple”)
由于可能冲突,所以需要顺着链表头节点一个个向下查找,匹配每个节点中key和要查找的key是否一致,一致就返回对应的value
之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

关键问题

  • HashMap默认初始长度是多少?

默认长度为16,且每次自动扩展或手动初始化时,长度必须为2的幂

原因:
为了服务于从Key映射到index的Hash算法
index = Hash(“apple”)中需要Hash函数是尽量均匀分布的,需要利用Key的HashCode值来做某种运算来实现。
为了高效实现Hash算法,采用了位运算的方式:
index = HashCode(Key) & (Length - 1)
就是直接取HashCode的最后几位二进制数

  • 高并发下为什么HashMap可能会出现死锁?

在ReHash时,当属于一个链表的连续节点在重映射后又同时映射到一个index时,会使得新HashMap中这两个节点产生循环引用,此时当要查询一个不存在的键值对,而这个键的映射index又正好是产生循环引用的节点所在的index时,就会出现死循环,从而导致查询无法结束,造成死锁现象。
原因:

ReHash是在HashMap的容量达到最大容量的一定比例时,需要进行的操作,该操作是为了减小冲突发生的几率。
衡量HashMap是否进行Resize的条件如下:
HashMap.Size >= Capacity * LoadFactor
大致的过程就是,申请新数组,将旧数组中的所有Entry重新计算HashCode,使用新数组长度重新计算对应的index,然后依次放入新的数组中。
ReHash的Java代码如下:

            /**
             * Transfers all entries from current table to newTable.
             */
            void transfer(Entry[] newTable, boolean rehash) {
                int newCapacity = newTable.length;
                for (Entry<K,V> e : table) {
                    while(null != e) {
        1               Entry<K,V> next = e.next;
                        if (rehash) {
                            e.hash = null == e.key ? 0 : hash(e.key);
                        }
                        int i = indexFor(e.hash, newCapacity);
        2               e.next = newTable[i];
        3               newTable[i] = e;
        4               e = next;
                    }
                }
            }

该流程在单线程下没有问题,但是在多线程时就有可能出现死锁现象。

当两个线程A和B,在同一时刻对HashMap进行Put,且此时HashMap已经到了Resize的临界点时,两个线程都开始Resize
old HashMap为:
这里写图片描述
当线程B执行完1,线程就被挂起。对于线程B来说:e = Entry3 ;next = Entry2
线程A此时开始执行并全部执行完Resize操作,此时A_New_HashMap为:
这里写图片描述
其中e和next为线程B中的引用示意
此时执行线程B,仔细分析代码,会发现当线程B执行完Resize操作后,A_New_HashMap和B_New_HashMap变为:
这里写图片描述
此时形成了循环链表,但是还暂时不会引起死锁问题。
但是当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!

解决方案:

一种常用的方案是,使用另一个集合类ConcurrentHashMap,这个集合类兼顾了线程安全和性能。

总结

  1. HashMap插入冲突时,使用头插法
  2. HashMap默认初始长度为16
  3. 扩展长度时,长度为2的幂
  4. Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
    HashMap.Size >= Capacity * LoadFactor。
  5. Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。

参考:
漫画:什么是HashMap?
漫画:高并发下的HashMap

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值