【再探】Java—HashMap原理

 HashMap 不是线程安全的。底层实现的数据结构是:数组 + 链表 + 红黑树。当根据key 来获取键值时,根据key 的哈希值来快速定位其在数组中的位置,然后再在这个元素中的链条(或红黑树)中进行查找。

HashMap 会自动扩展,但不会收缩。

1 数据结构

图 HashMap的内部结构

HashMap中一般是数组+链条的结构,只有当链条中的key个数达到阈值(8)时,才会把链表转换为红黑树(自平衡二叉查找树)。而当树中key的个数小于阈值(6)时,会把红黑树退化为链条。

图 存储K/V 的类的UML

1.1 自扩容

当哈希表中元素的数量超过其容量乘以加载因子的乘积时,HashMap会进行扩容。扩容时,是将当前容量扩大1倍。扩容步骤:

1)计算新容量(原容量*2),并创建新的数组。(旧数组为oldTab,新数组为newTab,旧容量为oldCap,新容量为newCap)。

2)从索引0开始,对原数组的每个位置进行遍历,如果该桶不为空,则将桶里的元素复制到新数组中。

a) 如果桶中第一个元素e的next 指针指向null,则newTab[e.hash & (newCap - 1)] = e。

b)其next指向不为空:

b1)将该桶的链条中key的hash值大于旧容量的元素组成一个高位链条,其头部元素设为hiHead。将该桶的链条中的key的hash值小于旧容量的元素组成一个低位链条,其头部元素设为loHead。

b2)newTab[j] = loHead; newTab[j+oldCap] = hiHead;

2 性能影响因素

HashMap 中的容量约定为2的n次方(n>0)。对于2的整数次幂取模:a % b = a &(b-1)。 b = 1 << k (k为整数)。

HashMap 数组最大长度为int 类型的最大值,int 在java中是32位。

2.1 hash 函数的设计

良好的hash函数要能让这些key均衡的落在其数组上,减少key的碰撞概率。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这是HashMap内部对key 进行hash 运算。将key 的高位(前16位)与低位(后16位)进行异或运算。将高位的影响传播到低位。这样,即使两个哈希值在高位上有所不同(低位相同的情况下,假如没有与高位的进行异或运算,那么它们在数组上的索引是一样的,那么将会发生碰撞),这些差异会传播到低位,从而增加了它们具有不同索引的可能性。

2.2 负载因子

负载因子 = 尺寸 / 容量。 负载因子可以在创建HashMap时指定,默认是0.75。当其尺寸增加导致负载因子达到设定值时,HashMap会进行扩展。

负载因子越高,意味着key的碰撞概率越高,插入及查找的性能就越低,而负载因子越低,虽然碰撞概率更低了,但是遍历的效率也会降低。

2.2.1 Key 的碰撞概率与柏松分布

柏松分布是用于描述单位时间内随机事件发生的次数的概率分布。

在HashMap 中,一个key 发生的概率并不是一个固定值,而是与其容量、加载因子、key的数量以及哈希函数有关。

假设哈希函数是完美的(key能均匀分布到数组中),负载因子为0.75,容量为2的整数次方,这时一个key是否发生碰撞的概率大概为0.5。

P(N(t)=n) 表示在单位时间t内,key 碰撞n次的概率,下面是不同的n对应的概率。

n

P

0

0.6065

1

0.3032

2

0.0758

3

0.0126

4

0.0015

5

0.0001

6

0.000013

7

0.0000009

8

0.00000006

表 碰撞n次的概率

当一个key碰撞8次时,它的概率千万分之六,已经非常低了。(8被设置为桶中链表转化为红黑树的阈值)。

3 功能与缺陷

3.1 HashMap 的 keySet()方法

图 HashMap 中存储key 的数据结构KeySet 的 UML

KeySet是HashMap 内部的非静态类。实现了遍历方法,遍历时会访问数组的每一个桶,如果该桶中包含元素,则会继续遍历桶里的元素。

3.2 非线程安全缺陷—死循环

在数据进行扩容时,如果此时有多个线程对其执行写操作,则有可能造成“死循环”问题。以下是两个线程扩容时同时对同一个桶进行复制的场景:

图 HashMap 扩容时死循环场景

3.2.1 ConcurrentHashMap

ConcurrentHashMap 是线程安全的,支持高效并发的。主要是通过Segment,段锁来实现。它将容器分成几个段(通常是16),每个段包含了若干桶。当执行写操作的时候,只需要对对应的段加段锁,其他段不加锁。

  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值