HashMap与ConcurrentHashMap

此文章为cs-notes的总结版,可以看作为我对cs-notes的学习笔记,原文请看CS-Notes

HashMap

1.存储结构

内部包含了一个 Entry 类型的数组 table

transient Entry[] table;

Entry 包含 4 个字段:

int hashCode
K key
V value
Entry<K,V> next

可以看出,Entry 是一个链表。Entry 类型的数组中存放着多个链表。HashMap 使用的是拉链法解决冲突,同一个链表中存放 hashCode 和数组大小取模运算结果相同的 Entry

2.拉链法的工作原理

插入

  • 新建一个 HashMap,默认大小为 16
  • 插入 <K1,V1> 键值对,先计算 K1 的 HashCode ,假设为115,进行取模运算得到桶下标115mod16=3
  • 插入 <K2,V2> 键值对,先计算 K2 的 HashCode ,假设为118,进行取模运算得到桶下标118mod16=6
  • 插入 <K3,V3> 键值对,先计算 K3 的 HashCode ,假设为118,进行取模运算得到桶下标118mod16=6,插在 <K2,V2> 前面。
    注意点:链表的插入是以头插法的方式进行的,<K3,V3> 是插在 <K2,V2> 链表的头部。

查找
查找分为 2 个步骤:

  • 计算键值对所在的桶(下标,即上文我们计算出的 36 );
  • 直接根据下标找到链表,在链表上顺序查找,显然时间复杂度与链表长度成正比。

3.put 操作

  • 判断是否为 nullHashMap 允许插入键为 null 的键值对。但是因为无法调用 nullhashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
  • 确定桶下标。
  • 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
  • 如果不存在,就使用链表的头插法。

4.确定桶下标

  • 计算 hash
  • hash 值取模。如果能够保证 capacity2n 次方,那么就将取模操作转换为位运算(位运算的代价比取模运算小得多)。位运算或者取模运算的结果就是桶下标。

5.扩容-基本原理

  • HashMaptable 的长度为 M ,需要存储的键值对的数量为 N ,如果 hash 函数满足均匀性要求,那么每条链表的长度为 N/M ,因此平均查找的时间复杂度为 O(N/M)
  • 为了让查找的成本降低,应该使 N/M 尽可能小,因此要让 M 尽可能大。 HashMap 采用动态扩容,根据当前的 N 来调整 M 的值,使得空间和时间效率得到保证。
  • 需要扩容时,将 capacity 扩大为原来的 2 倍(保证 capacity2n 次方,确定桶下标时,能够采用位运算,加快速度)。
  • oldTable 里面的所有键值对重新计算桶下标,再插入到 newTable 里面,所以扩容是非常费时的。

6.扩容-重新计算桶下标

  • 如果 capacity2n 次方,那么扩容时能够极大地降低重新计算桶下标的复杂度。

7.计算数组容量

  • HashMap 构造函数允许用户传入的容量不是 2n 次方,因为它可以自动地将传入的容量转换为 2n 次方。

8.链表转红黑树

  • JDK 1.8 开始,一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。

9.与HashTable比较

  • HashTable 使用 synchronized 来进行同步。
  • HashMap 可以插入键值对为 nullEntry
  • HashMap 的迭代器是 fail-fast 迭代器。
  • HashMap不能保证随着时间的推移,Map 中的元素次序是不变的。

ConcurrentHashMap

1.存储结构

static final class HashEntry<K,V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K,V> next;
}
  • ConcurrentHashMapHashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
  • 默认的并发级别为 16,也就是说默认创建 16Segment

2.size操作

  • 每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。
  • 由于 ConcurrentHashMap 使用的是分段锁,所以在计算一段的 size 时,另外几段可能发生变化,这就使得 size 发生变化。
  • ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。
  • 如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。

3.JDK 1.8 的改动

  • JDK 1.8 使用了 CAS(Compare And Swap) 操作来支持更高的并发度,在 CAS 操作失败时,使用内置锁 synchronized
  • JDK 1.8 的实现也在链表过长时会转换为红黑树。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值