聊一聊java中的HashMap,ConcurrentHashMap以及Collections.SynchronizedMap

什么是红黑树?

鉴于一定会提到红黑树,那么就先来了解了解它吧。

红黑树(Red Black Tree)是一种自平衡的二叉查找树,是在计算机科学中常用的一种数据结构,典型的用途是实现关联数组。它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees),后来在1978年被Leo J. Guibas和Robert Sedgewick修改为如今的红黑树。

红黑树具有如下性质:

  1. 每个节点都有一个颜色属性,可以是红色或黑色。
  2. 根节点是黑色的。
  3. 所有叶子节点(NIL节点或空节点)都是黑色的。
  4. 如果一个节点是红色的,则它的两个子节点都是黑色的。
  5. 从任一节点到其每个叶子节点的所有路径都包含相同数量的黑色节点,这被称为黑色节点的“路径长度相等”。
  6. 红黑树的最长路径不会超过最短路径的两倍,这确保了红黑树的高度始终在对数级别。

由于红黑树的这些性质,它在插入、删除和查找操作时的最坏情况运行时间非常良好,能在O(log n)时间内完成。因此,红黑树在实践中是高效的,特别适用于搜索、插入和删除操作较多的情况。

什么是链表?

同样,简单再回忆下链表。

链表(Linked List)是一种常见的数据结构,它由一系列节点(Node)组成,每个节点包含两部分:数据域和指针域。数据域用于存储元素值,而指针域则用于指向链表中的下一个节点。链表中的第一个节点被称为头节点(Head),最后一个节点通常指向空(Null),表示链表的结束。

链表的主要特点包括:

  1. 动态分配:链表的大小可以在运行时动态地增长或缩小,不需要预先分配固定大小的内存空间。

  2. 插入和删除效率高:在已知某一节点位置的情况下,链表的插入和删除操作通常只需要修改相邻节点的指针,因此效率相对较高。

  3. 顺序访问:链表中的元素是按照线性顺序链接在一起的,访问某个元素需要从头节点开始依次遍历。

  4. 内存利用率高:链表可以充分利用计算机内存中的碎片空间,不需要像数组那样要求连续的内存块。

链表有多种类型,其中最常见的是单向链表(Singly Linked List)和双向链表(Doubly Linked List)。

  • 单向链表:每个节点只有一个指针,指向下一个节点。单向链表只能从头节点开始,沿一个方向遍历整个链表。

  • 双向链表:每个节点有两个指针,一个指向前一个节点,另一个指向后一个节点。双向链表可以从任意节点开始,向前或向后遍历链表。

此外,还有循环链表(Circular Linked List),它的尾节点指针不是指向空,而是指回头节点,形成一个闭环。

链表在实际应用中有广泛的用途,例如用于实现栈、队列等数据结构,以及在需要频繁进行插入和删除操作的场景中作为数据的存储结构。然而,链表的随机访问效率较低,因为需要从头节点开始遍历才能找到特定位置的元素。因此,在选择使用链表时需要根据具体需求进行权衡。

链表与红黑树对比:

结构和特性

  • 链表:链表由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。链表可以动态地改变其长度,并且可以在任何位置插入或删除节点。这种灵活性使得链表在处理插入或删除操作时更加高效。
  • 红黑树:红黑树是一种特殊的平衡二叉搜索树,它具有一系列特定的性质,如节点颜色(红色或黑色)、根节点必须是黑色、叶节点(NIL)是黑色的等。这些性质保证了红黑树在插入、删除和查找操作中都能保持相对平衡,从而确保这些操作的时间复杂度为O(log n)。

查找效率

  • 链表的查找效率较低,因为需要从头节点开始遍历整个链表来找到目标节点,时间复杂度为O(n)。
  • 红黑树的查找效率较高,由于它的平衡性,可以确保查找路径相对较短,时间复杂度为O(log n)。

插入和删除操作

  • 在链表中插入和删除节点相对容易,只需要更新节点的指针即可。然而,如果链表很长,那么找到要插入或删除的位置可能会很耗时。
  • 红黑树的插入和删除操作虽然相对复杂,但由于其平衡性,这些操作的时间复杂度仍然是O(log n)。

内存使用

  • 链表在存储元素时需要额外的空间来存储节点之间的连接关系,这会增加一定的内存消耗。然而,链表可以充分利用内存碎片,提高内存的利用率。
  • 红黑树在存储元素时不需要额外的空间来存储节点之间的连接关系,但其节点结构相对复杂,每个节点包含更多的信息(如颜色、父节点指针等)。

适用场景

  • 链表常用于需要频繁进行插入和删除操作的数据集合,特别是在不需要频繁查找的情况下。此外,链表也常用于实现自定义的内存管理器、事件处理、深度优先搜索等场景。
  • 红黑树则更适用于需要频繁进行查找、插入和删除操作的数据集合。由于其平衡性,红黑树在数据库系统、操作系统、图形处理系统等多个领域都有广泛的应用。

综上所述,链表和红黑树各有其优势和适用场景。在选择使用哪种数据结构时,需要根据具体的应用需求和场景来进行权衡和选择。

OK,接下来进入正题,一起来看看java中的HashMap,ConcurrentHashMap以及Collections.SynchronizedMap

HashMap

Java 7 中的 HashMap

实现原理

  1. 数据结构:Java 7 中的 HashMap 使用数组和链表来存储键值对。当某个桶(bucket)的链表长度达到阈值(默认为 8)时,链表会转换为红黑树。但 Java 7 中实际上并没有实现红黑树的转换,这是 Java 8 中的一个改进。
  2. 哈希函数:通过键的 hashCode() 方法计算哈希值,然后通过哈希值确定键在数组中的位置。
  3. 扩容:当 HashMap 中的元素数量超过数组容量的一定比例(加载因子,默认为 0.75)时,会进行扩容,即创建一个新的数组,并将原数组中的元素重新计算哈希值后放入新数组。

Java 8 中的 HashMap

实现原理

  1. 数据结构:Java 8 中的 HashMap 在 Java 7 的基础上进行了优化。当某个桶的链表长度达到阈值(默认为 8)时,链表会转换为红黑树,以提高查找效率。当树的大小小于 6 时,又会退化为链表
  2. 哈希函数:与 Java 7 相同,通过键的 hashCode() 方法计算哈希值。
  3. 扩容:与 Java 7 相同,当元素数量超过数组容量的一定比例时,会进行扩容。

变化

  • 引入了红黑树,优化了性能。
  • 链表转红黑树的阈值是 8,红黑树转链表的阈值是 6。

ConcurrentHashMap

Java 7 中的 ConcurrentHashMap

实现原理

  1. 分段锁ConcurrentHashMap 在 Java 7 中使用分段锁(Segment)来实现并发控制。每个 Segment 维护着自己的锁和哈希表,多个线程可以并发地操作不同的 Segment。
  2. 哈希函数和扩容:与 HashMap 类似,通过键的 hashCode() 方法计算哈希值,并确定键在数组中的位置。当某个 Segment 中的元素数量超过容量的一定比例时,会进行扩容。

Java 8 中的 ConcurrentHashMap

实现原理

  1. CAS 和同步控制:Java 8 中的 ConcurrentHashMap 放弃了分段锁的设计,转而使用 CAS(Compare-and-Swap)操作和精细化的同步控制来实现更高的并发性能。
  2. Node 数组和链表/红黑树:与 HashMap 类似,ConcurrentHashMap 也使用数组和链表/红黑树来存储键值对。当某个桶的链表长度达到一定阈值时,会转换为红黑树。
  3. 扩容:当整个 ConcurrentHashMap 的元素数量超过数组容量的一定比例时,会进行扩容。在扩容过程中,会利用 CAS 操作和同步控制来确保线程安全。

变化

  • 放弃了分段锁的设计,使用 CAS 和同步控制实现更高的并发性能。
  • 引入了红黑树,优化了性能。
  • 扩容机制也有所改变,更加高效和线程安全。

总结来说,Java 8 对 HashMap 和 ConcurrentHashMap 都进行了优化,引入了红黑树来提高性能,并在 ConcurrentHashMap 中放弃了分段锁的设计,使用 CAS 和同步控制实现更高的并发性能。这些改进使得 Java 8 中的集合框架在并发和性能方面更加出色。

Collections.SynchronizedMap

Collections.synchronizedMap 是 Java 集合框架中提供的一个工具方法,用于创建一个线程安全的 Map。它接受一个现有的 Map 对象作为参数,并返回一个通过同步包装后的 Map。这个同步的 Map 在对其进行操作期间,其他线程无法修改其内容,从而确保了线程安全性。

当多个线程同时访问一个普通的 Map 时,可能会导致数据不一致的问题。为了避免这种问题,可以使用 Collections.synchronizedMap 方法来创建一个同步的 Map。这样,每个对 Map 的操作(如添加、删除、修改等)都会是原子的,从而保证了数据的一致性。

使用 Collections.synchronizedMap 创建的同步 Map 主要有以下特点:

  1. 线程安全:通过同步机制确保了对 Map 的操作是线程安全的。
  2. 原子性:对 Map 的每个操作都是原子的,即在执行过程中不会被其他线程打断。
  3. 互斥性:在对 Map 进行操作时,其他线程无法修改其内容。

需要注意的是,虽然 Collections.synchronizedMap 提供了线程安全的 Map,但在使用它时仍需要谨慎处理并发问题。例如,当迭代遍历 Map 时,即使使用了同步的 Map,如果在迭代过程中有其他线程修改了 Map,那么迭代结果可能会不一致。为了避免这种情况,可以在迭代时使用同步块来确保整个迭代过程的线程安全性。

总的来说,Collections.synchronizedMap 是一个方便且实用的工具,用于在 Java 中创建线程安全的 Map。但在使用时,仍需要根据具体场景和需求来谨慎处理并发问题。

关于扩容的进一步说明:

HashMap的扩容过程主要发生在其内部的数组无法容纳更多键值对时。以下是HashMap扩容的详细步骤:

  1. 判断是否需要扩容:当HashMap中的元素数量(size)超过了数组长度(capacity)与负载因子(load factor)的乘积时,就会触发扩容。负载因子是一个衡量HashMap是否需要扩容的阈值,其默认值为0.75。因此,扩容的条件可以表示为:size > capacity * loadFactor
  2. 创建新的数组:一旦确定需要扩容,HashMap会创建一个新的数组,其容量通常是原数组容量的两倍。这样做的好处是,新的数组可以容纳更多的元素,同时保持了相对较低的哈希冲突概率。
  3. 重新计算哈希值并迁移元素:扩容后,HashMap中的每个键值对都需要重新计算哈希值,并根据新的哈希值确定在新数组中的位置。这个过程涉及到大量的元素迁移。对于每个键值对,HashMap会计算其键的哈希值,并使用这个哈希值来确定在新数组中的索引位置。然后,将键值对放入新数组对应索引位置的桶中。

在Java 8及以后的版本中,HashMap引入了红黑树来优化性能。当某个桶中的链表长度超过一定阈值(默认为8)时,链表会转换为红黑树,以提高查找效率。在扩容过程中,如果原桶中的结构是红黑树,那么扩容后对应的桶也会保持红黑树的结构。

需要注意的是,HashMap的扩容操作是一个相对耗时的过程,因为它涉及到大量元素的迁移和重新哈希。因此,在初始化HashMap时,如果能预估到未来可能存储的数据量,合理设置初始容量(initial capacity)和负载因子,可以有效减少扩容的频率和性能损失。

另外,HashMap的扩容并不是线程安全的。如果在多线程环境下使用HashMap,并且多个线程同时修改HashMap,可能会导致数据不一致。如果需要线程安全的HashMap,可以考虑使用ConcurrentHashMap。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值