前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >HashMap的数据结构浅析[通俗易懂]

HashMap的数据结构浅析[通俗易懂]

作者头像
全栈程序员站长
发布2022-08-30 20:40:58
4680
发布2022-08-30 20:40:58
举报
文章被收录于专栏:全栈程序员必看

大家好,又见面了,我是你们的朋友全栈君。

HashMap是非线程安全的。而HashMap的线程不安全主要体现在resize时的死循环

HashMap工作原理

  • HashMap数据结构 常用的底层数据结构主要有数组和链表。数组存储区间连续,占用内存较多,寻址容易,插入和删除困难。链表存储区间离散,占用内存较少,寻址困难,插入和删除容易。
  • HashMap要实现的是哈希表的效果,尽量实现O(1)级别的增删改查。它的具体实现则是同时使用了数组和链表,可以认为最外层是一个数组,数组的每个元素是一个链表的表头。

HashMap的寻址方式

  • 对于新插入的数据或者待读取的数据,HashMap将Key的哈希值对数组长度取模,结果作为该Entry在数组中的index。因此数组的长度必须时2的N次方,实际中,HashMap会自动通过Integer.highestOneBit算出比指定整数大的最小的N值。
代码语言:javascript
复制
        public static int highestOneBit(int i) {
          i |= (i >>  1);
          i |= (i >>  2);
          i |= (i >>  4);
          i |= (i >>  8);
          i |= (i >> 16);
          return i - (i >>> 1);
        }
  • 碰撞:由于数组的长度是有限的,所以不管Hash()方法和Equals()方法写得如何严谨,始终不能完全避免碰撞的发生(Hash值算出来的Index相同,需要放在数组的同一个位置),碰撞发生后,我们就只能添加一个链表子节点,但是这无疑会降低查找效率(找到Index还要遍历链表。。。)
  • 加载因子:默认是0.75,当数组的被占空间>=0.75的时候,HashMap就会进行扩容(变为两倍),扩容之后数组中的所有元素进行重排序(算出来的Index可能不同),从而减少数组下链表的长度,提高查找效率。

Java8 中的升级

  • Java8 中的HashMap采用了数组+链表+红黑树(类似于数据结构中的平衡二叉树)的模式(当链表长度<8时仍然采用链表的形式,>8时由链表的数据结构转换成红黑树的数据结构)

JDK1.7中线程不安全的HashMap

  • 上文讲到占用空间超过加载因子的值时,就会自动扩容,这时HashMap中的元素或重新计算排序,这显然是不能保证线程安全的,而且在多线程并发调用时,可能出现死循环。
  • 首先:先给出resize的核心代码:
代码语言:javascript
复制
void transfer(Entry[] newTable, boolean rehash) {  
        int newCapacity = newTable.length;  
        for (Entry<K,V> e : table) {  
  
            while(null != e) {  
                Entry<K,V> next = e.next;           
                if (rehash) {  
                    e.hash = null == e.key ? 0 : hash(e.key);  
                }  
                int i = indexFor(e.hash, newCapacity);   
                e.next = newTable[i];  
                newTable[i] = e;  
                e = next;  
            } 
        }  
    }  
  • 在多线程调用中,可能会产生这种情况:两个线程同时认为HashMap需要rehash,这里就有一种可能性,这两个线程在调用的时候
  • 当两个线程痛时执行的时候,可能有一个会被挂起,等待另一个结束后在继续执行,这时问题就产生了;
  • 产生死循环的原因是:while(null != e)这句判断,在第一次执行的时候,B原本->null,于是重新计算到B的时候B->null,判断体就结束执行,于是就扩容成功
  • 当第二个线程执行扩容的时候,内存中是B->A ,是的,条件成立,而原本的条件(线程2以为的条件)是A->B 这里,两个条件同时成立,后果就是。。。一直循环下去A->B->A->B…
    • 具体过程是A->B,执行while,将A头插到数组,指针指向下一个,B->A,条件成立,将B头插如数组,指针指向下一个,A->B…
JDK1.8的优化
  • 在JDK1.8中采用了尾插法,可以有效避免上述这种死循环的情况。

以上就是我目前的理解,还有一种使用迭代器时的fast-fail,以后有机会更新。 关于ConcurrentHashMap可以看看我的另外一篇,欢迎指正:ConcurrentHashMap浅析

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/144696.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • HashMap工作原理
  • HashMap的寻址方式
    • Java8 中的升级
      • JDK1.7中线程不安全的HashMap
        • JDK1.8的优化
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档