当前位置: 首页 > news >正文

杭州网站建设q479185700棒辽宁建设工程招标网

杭州网站建设q479185700棒,辽宁建设工程招标网,浙江英文网站建设,wordpress 结合qq文章目录1. HashMap概述2. 哈希冲突3. 树化与退化3.1 树化的意义3.2 树的退化4. 二次哈希5. put方法源码分析6. key的设计7. 并发问题参考 如何防止因哈希碰撞引起的DoS攻击_hashmap dos攻击_双子孤狼的博客-CSDN博客 为什么 HashMap 要用 h^(h >>&#…

文章目录

  • 1. HashMap概述
  • 2. 哈希冲突
  • 3. 树化与退化
    • 3.1 树化的意义
    • 3.2 树的退化
  • 4. 二次哈希
  • 5. put方法源码分析
  • 6. key的设计
  • 7. 并发问题

参考

  • 如何防止因哈希碰撞引起的DoS攻击_hashmap dos攻击_双子孤狼的博客-CSDN博客
  • 为什么 HashMap 要用 h^(h >>>16) 计算hash值?槽位数必须是 2^n?_一行Java的博客-CSDN博客
  • HashMap面试,看这一篇就够了_苦味代码的博客-CSDN博客

1. HashMap概述

HashMap是Java中最常用的集合框架。

在JDK1.7的时候,HashMap的底层由数组和链表组成。数组是HashMap的主体,而链表是为了解决哈希冲突而存在的。

image-20230203164757901

在JDK1.8的时候,HashMap的底层由数组、链表和红黑树组成。红黑树的出现是为了解决因哈希冲突导致的链表长度过长影响HashMap性能的问题。红黑树搜索的空间复杂度为O(logn),而链表却是O(n)

也就是当链表的长度达到一定长度后,链表就会进行树化,当然这是一种笼统说法,具体细节待会深究。

img


2. 哈希冲突

哈希冲突是指对不同的关键字通过一个哈希函数进行计算得出相同的哈希值,这样使得它们存在的数组时候发生了冲突。

解决哈希冲突通常有以下四种方法。

  • 开放定址法

开放定址法,也称为再散列地址法。基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hashp1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi

就是说当发生哈希冲突的时候,对哈希值进行求哈希值,只要哈希表足够大,那么总能找到一个这样的空地址。

因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素。

  • 再哈希法

再哈希法,也称双重散列,多重散列。基本思想就是提供多个不同的哈希函数,当R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。

  • 链地址法

链地址法,也称拉链法,将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。

  • 建立公共溢出区

将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。

HashMap采用的就是链地址法。

并且JDK1.7和JDK1.8的链表插入节点时,采用的方式不一样

  1. JDK1.7采用的是头插法。
  2. JDK1.8采用的是尾插法。

3. 树化与退化

树化是指将链表转化为红黑树的过程。

在JDK1.8的HashMap中,树化的规则是这样的:当链表长度超过树化阈值 8 时,先尝试扩容来减少链表长度,如果数组容量已经 >=64,才会进行树化

所以,HashMap并不是一开始就进行树化的。

阈值设置为8主要是因为泊松分布,具体原因HashMap作者在源码中也有解释

image-20230203172039652

意思就是说,理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

也就是说哈希 值如果足够随机,则在 哈希表内按泊松分布,在负载因子 0.75 的情况下,长度超过 8 的链表出现概率是 0.00000006,树化阈值选择 8 就是为了让树化几率足够小。


3.1 树化的意义

首先,树化成红黑树可以避免DOS攻击,防止链表超长时性能下降,树化应当是偶然情况,是保底策略。

DOS攻击是指恶意的攻击者通过使用以下精心构造的数据,使得所有数据经过哈希函数之后,都映射到了一个位置,导致了大量数据出现了哈希冲突,通过HashMap查询的效率从O(1)变成了O(n)。这样就有可能发生因为查询操作消耗大量 CPU 或者线程资源,而导致系统无法响应其他请求的情况,从而达到拒绝服务攻击(DoS)的目的。

其次,哈希表的查找,更新的时间复杂度是 O(1),而红黑树的查找,更新的时间复杂度是 O(log2⁡n)

但是由于TreeNode 占用空间也比普通 Node 的大,如非必要,尽量还是使用链表。


3.2 树的退化

树的退化主要是发生在这两种情况下

  • HashMap在扩容时,如果拆分树,树元素个数小于等于6,则会退化成链表
  • remove 树节点时,若 rootroot.leftroot.rightroot.left.left 有一个为 null ,也会退化为链表

4. 二次哈希

在JDK1.8的源码的put方法中,会将key进行二次哈希

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

通过源码可以了解到,首先会调用key对象的hashCode方法进行求哈希值,然后将该哈希值的低16位与高16位进行异或运算,这样做的目的是为了综合高位数据,让哈希分布更为均匀,减少哈希碰撞。

并且这么做可以在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

操作
hashCode()1,794,106,052
二进制01101010 11101111 11100010 11000100
h >>> 1600000000 00000000 01101010 11101111

5. put方法源码分析

查看put方法源码

transient Node<K,V>[] table;public V put(K key, V value) {// 调用上文我们已经分析过的hash方法return putVal(hash(key), key, value, false, true);
}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)// 第一次put时,会调用resize进行桶数组初始化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;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);// 链表过长,转换为树结构if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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;
}

put方法的大体思路如下

  1. 调用keyhashCode方法计算哈希值,并据此计算出数组下标index
  2. 如果发现当前的桶数组为null,则调用resize()方法进行初始化
  3. 如果没有发生哈希碰撞,则直接放到对应的桶中
  4. 如果发生哈希碰撞,且节点已经存在,就替换掉相应的value
  5. 如果发生哈希碰撞,且桶中存放的是树状结构,则挂载到树上
  6. 如果碰撞后为链表,添加到链表尾,如果链表超度超过TREEIFY_THRESHOLD默认是8,则将链表转换为树结构
  7. 数据put完成后,如果HashMap的总数超过threshold就要resize

put方法中比较重要的一个知识点莫过于计算索引了。

  1. 首先,先调用hash方法。
    1. hash方法中,计算对象的hashCode方法
    2. 再进行调用HashMaphash()方法进行二次哈希
  2. 接着,将hash方法返回的二次哈希值记为hash,用(n - 1)也就是数组长度减一对hash进行与运算((n - 1) & hash

这里使用n-1是因为以默认数组长度16为例子,那么数组下标为0-15,哈希值计算hash%(2^4),其本质就是和长度取余。也就等价于 (2^4 - 1) & hash

在JDK1.8和JDK1.7中,它们的put方法实现有所不同

  1. 链表插入节点时,1.7 是头插法,1.8 是尾插法

  2. 1.7 是大于等于阈值且没有空位时才扩容,而 1.8 是大于阈值就扩容

  3. 1.8 在扩容计算 Node 索引时,会优化


6. key的设计

HashMap key 可以为 null,但 Map 的其他实现就不一定了

key应当符合下面的要求

  1. 作为 key 的对象,必须实现 hashCode equals,并且 key 的内容不能修改(不可变)
  2. key 的 hashCode 应该有良好的散列性

7. 并发问题

  • 扩容死链(1.7 会存在)
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;}}
}
  • enext 都是局部变量,用来指向当前节点和下一个节点
  • 线程1(绿色)的临时变量 enext刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程2(蓝色)完成扩容和迁移

image-20210831084325075

  • 线程2 扩容完成,由于头插法,链表顺序颠倒。但线程1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移

image-20210831084723383

  • 第一次循环
    • 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
    • e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
    • 当循环结束是 e 会指向 next 也就是 b 节点

image-20210831084855348

  • 第二次循环
    • next 指向了节点 a
    • e 头插节点 b
    • 当循环结束时,e 指向 next 也就是节点 a

image-20210831085329449

  • 第三次循环
    • next 指向了 null
    • e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
    • 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出

image-20210831085543224

  • 数据错乱(1.7,1.8 都会存在)

假设现在有两个线程A和B,这两个线程都分别将数据C和D放进HashMap中,并且C和D的二次哈希值都一样。

线程A和线程B同时执行到检查有无哈希冲突的那一段代码。A和B检查均无发现有哈希冲突。

假设线程A比较快,于是线程A将tab[i]指向数据C。

这时候线程B将tab[i]指向数据D。

最终tab[i]指向数据D,导致了数据C丢失


http://www.yayakq.cn/news/820136/

相关文章:

  • 免费视频素材下载的网站大连网页制作wordpress
  • 网站建设标准合同书ipv6可以做网站吗
  • 菏泽网站建设效果网站推广话术
  • 百度上找不到网站代做网站的公司
  • 大学生网站开发总结报告潍坊网站建设 管雷鸣
  • 秦皇岛建设网站官网做网站建设销售途径
  • 石家庄php网站建设已有域名 搭建网站
  • 好看的中文网站设计wordpress主题 视频教程
  • 开发网站公司怎么样如何建设软件下载网站
  • psd做模板下载网站做百度网站需要什么条件
  • 建站导航企业网站推广的方法有什么
  • 呼伦贝尔网站设计广东省新闻
  • 织梦做企业网站3d制图软件
  • 向雅虎提交网站网站备案收费标准
  • 公司官网推广上海优化seo
  • 哪个网站是专门做封面素材定西模板型网站建设
  • 高端文化网站模板网页超链接制作
  • 网络集资网站怎么做河源wordpress培训
  • 成都工程建设信息网站青岛的互联网公司
  • 吴江市中云建设监理有限公司网站嘉兴网站建设兼职
  • 网站开发前端简历西安网站排名优化培训
  • 品牌营销网站建设网站做个seo要多少钱
  • 个人网站如何做淘宝客织梦商城网站源码
  • 上海网站建设 报价宁波网站推广工作室电话
  • 买域名后 怎么做网站网站开发虚拟电话
  • wordpress开放目录东莞seo建站优化公司
  • 素材网站推广方案qpython3手机版
  • 163网易企业邮箱格式悟空建站seo服务
  • 河北怎样做网站三维设计官网
  • 学校网站前置审批医疗教育的网站建设