HashMap、HashTable和ConcurrentHashMap
悲观者从机会中看到困难。乐观者从困难中看到机会。
——温斯顿·丘吉尔
HashMap
-
实现: 基于哈希表的数据结构,使用链地址法解决哈希冲突。从 Java 8 开始,当链表长度超过一定阈值时,链表会转换成红黑树,以提高查询性能。
-
线程安全:
HashMap是非线程安全的。在多线程环境下,如果多个线程同时对HashMap进行操作,可能会导致数据不一致或死循环等问题。 -
性能: 由于
HashMap没有同步机制,它在单线程环境中性能非常高。插入、删除和查找操作的平均时间复杂度为 O(1)。 -
允许
null键和值:HashMap允许一个null键和多个null值。HashMap的初始化HashMap的初始化主要有以下两种方式:- 默认构造函数:使用默认容量(16)和负载因子(0.75)。
- 指定初始容量和负载因子:用户可以指定初始容量和负载因子
初始化流程:
-
默认情况下,
HashMap的容量(initialCapacity)为 16,负载因子(loadFactor)为 0.75。这意味着当HashMap中的元素数量达到容量的 75% 时,便会触发扩容。 -
构造函数只会设置容量和负载因子,并不会真正分配数组空间。真正分配数组是在第一次调用
put()方法时发生的。put流程put方法是HashMap用来插入键值对的核心方法。put的流程如下:- 计算哈希值: 调用
hash(key)方法计算key的哈希值,并通过indexFor(hash, length)方法计算出该键值对应在数组中的索引。 - 判断是否需要初始化数组: 如果数组
table为空,进行第一次初始化,分配数组空间。 - 检查冲突: 根据计算出的索引位置,检查数组中是否已经存在元素。如果该位置没有元素(即
null),则直接插入。如果存在元素,则遍历链表(或红黑树)查找是否有相同的key。 - 插入新元素: 如果找到了相同的
key,则更新该key对应的值;如果没有找到相同的key,则将新的Entry(Java 8 之后是Node)插入到链表的头部或转换为红黑树。 - 检查是否需要扩容: 插入后,检查当前
HashMap的元素个数是否超过了扩容阈值(即容量 * 负载因子)。如果超过,则进行扩容。
get流程get方法用于根据key获取对应的value。其流程如下:- 计算哈希值: 和
put类似,首先计算key的哈希值,并通过indexFor(hash, length)方法计算出该键值对应在数组中的索引。 - 查找元素: 根据计算出的索引位置,找到对应的链表(或红黑树),遍历链表或树查找与
key相同的元素。 - 返回结果: 如果找到了对应的
key,则返回对应的value;如果没找到,则返回null。
- 计算哈希值: 调用
Hashtable
- 实现: 与
HashMap类似,也是基于哈希表实现的,但它的所有方法都是同步的,使用了synchronized关键字来保证线程安全。 - 线程安全:
Hashtable是线程安全的,多个线程可以安全地访问同一个Hashtable实例而不会导致数据不一致的问题。然而,由于同步的开销,在高并发情况下性能较低。 - 性能: 由于同步机制,
Hashtable的性能要比HashMap低,尤其是在高并发环境下。 - 不允许
null键和值:Hashtable不允许null键或null值,试图插入null键或值会抛出NullPointerException。
ConcurrentHashMap
- 实现: 基于分段锁(Segmented Lock)或(从 Java 8 开始)CAS(Compare-And-Swap)机制的哈希表,设计上更适合高并发场景。在 Java 8 之前,
ConcurrentHashMap将整个哈希表分为多个段(Segment),每个段相当于一个小的哈希表,使用锁机制保证每个段的线程安全,多个线程可以并发访问不同的段。在 Java 8 及之后,ConcurrentHashMap使用了更为细粒度的锁机制和无锁(CAS)操作,使其性能进一步提升。 - 线程安全:
ConcurrentHashMap是线程安全的,并且在高并发环境中表现出色。它在读取操作时不需要锁,因此读取速度非常快。写操作使用了细粒度的锁或无锁机制,减少了锁争用,提高了并发性能。 - 性能: 在高并发场景中,
ConcurrentHashMap的性能要远高于Hashtable和同步包装的HashMap(如Collections.synchronizedMap(new HashMap<>())。 - 不允许
null键和值: 与Hashtable类似,ConcurrentHashMap也不允许null键或值。

浙公网安备 33010602011771号