前言
在 Java 集合框架中,HashMap 和 ConcurrentHashMap 是两种非常重要的哈希表实现。尽管它们的核心功能类似,都是通过键的哈希值将键映射到对应的桶(bucket),但它们的设计目标和实现细节却大相径庭。尤其是在 哈希值的处理 以及 桶索引的计算 上,二者采取了不同的策略。
本文将详细解析 为什么 ConcurrentHashMap 需要显式地将负数哈希值转化为正数,而 HashMap 可以在计算索引时自动解决负数问题。
HashMap 的哈希值与索引计算
HashMap 是一种非线程安全的哈希表,通常用于单线程环境。它通过哈希值将键映射到对应的桶(即数组的索引),并通过链表或红黑树解决哈希冲突。HashMap 的索引计算非常简单,具体步骤如下:
- 获取哈希值:
HashMap调用键对象的hashCode()方法来获取哈希值,可能是正数也可能是负数。 - 计算桶索引:
HashMap通过& (n - 1)来计算哈希值对应的桶索引,其中n是桶数组的长度,且始终为 2 的幂。
int hash = key.hashCode();
int index = (n - 1) & hash;
HashMap 如何自动处理负数哈希值
HashMap 的设计非常巧妙,它依赖 位与运算(&)将哈希值转化为桶索引。由于桶数组的长度 n 总是 2 的幂,n - 1 在二进制表示中全为 1。例如,若 n = 16,那么 n - 1 = 15,二进制为 1111。
通过 & (n - 1),只保留了哈希值的低位信息,并忽略了高位(包括符号位)。即使哈希值是负数,由于位与运算只处理哈希值的低几位,最终计算出的桶索引依然是非负数。
示例:
假设 key.hashCode() = -5,二进制表示为:11111111111111111111111111111011。桶的大小为 16(n = 16),此时 n - 1 = 15,二进制表示为 00000000000000000000000000001111。通过位与运算计算桶索引:
15 (二进制): 00000000000000000000000000001111
-5 (二进制): 11111111111111111111111111111011
----------------------------------------------
index (结果): 00000000000000000000000000001011
最终,桶索引为 11,即使哈希值是负数,HashMap 依然能够正常映射到有效的正整数索引。
ConcurrentHashMap 的哈希值与索引计算
ConcurrentHashMap 是一个线程安全的哈希表,设计用于高并发环境。与 HashMap 不同,它在处理哈希值时采取了更复杂的策略,目的是 提高并发操作的性能,尤其是减少哈希冲突。
哈希值的处理流程
在 ConcurrentHashMap 中,哈希值的处理分为两步:
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
高位和低位混合:h ^ (h >>> 16) 将哈希值的高 16 位与低 16 位进行异或运算。这个操作的目的是打乱哈希值,使其高位和低位都得到充分利用,减少某些键的哈希值过于集中在低位,导致分布不均。
限制哈希值为正数:& HASH_BITS 操作用于限制哈希值为非负数。HASH_BITS 是 0x7FFFFFFF(即 31 个 1),通过位与运算将哈希值的最高位(符号位)置为 0,确保哈希值是正数。
为什么 ConcurrentHashMap 需要显式处理负数
与 HashMap 不同,ConcurrentHashMap 的设计目标是在高并发场景下提供更好的性能。这意味着它必须尽量减少多个线程同时访问同一个桶,避免锁竞争。为此,它需要确保哈希值的分布尽可能均匀。
ConcurrentHashMap 通过 高位和低位混合 的方式来提高哈希值的分布均匀性。然而,这一步骤可能会生成负数哈希值。因此,它必须通过 & HASH_BITS 显式将负数转化为正数,避免负数哈希值对哈希表的索引分布产生不利影响。
为什么 HashMap 不需要显式处理负数
位与运算天然处理负数
HashMap 使用 & (n - 1) 位与运算来计算桶索引,这个操作非常简单而有效。由于 n - 1 在二进制中是全 1,位与运算会自动忽略哈希值的高位(包括符号位),只处理低位。这意味着即便 key.hashCode() 返回负数,位与运算也能将其转换为一个非负的有效桶索引。
因此,HashMap 不需要像 ConcurrentHashMap 那样显式处理负数哈希值。
没有并发需求
HashMap 是一个非线程安全的集合,不需要像 ConcurrentHashMap 那样处理高并发场景下的锁竞争问题。即使哈希值的分布不均匀,对 HashMap 来说,唯一的后果是链表或红黑树长度增加,查询效率下降,但不会有并发问题。
因此,HashMap 的设计可以更加简单,它依赖 hashCode() 和 & (n - 1) 的自然运算来处理负数和正数哈希值。
如果 ConcurrentHashMap 不处理负数,可能出现什么问题
如果 ConcurrentHashMap 不显式通过 & HASH_BITS 处理负数,可能会导致以下问题:
- 哈希分布不均匀:在
ConcurrentHashMap中,哈希值的高低位混合增加了哈希分布的均匀性。但如果生成的哈希值为负数,这可能会导致哈希值集中在某个范围内,增加某些桶的负担,导致哈希冲突增加。 - 锁争用问题:高并发环境下,哈希冲突增加意味着多个线程会同时争夺同一个桶的锁,导致锁的争用,影响并发性能。
- 索引越界风险:虽然负数哈希值可能被正确映射到桶索引上,但负数在某些场景下可能会导致不可预期的行为。因此,
ConcurrentHashMap需要保证哈希值为正数,确保安全可靠的并发操作。
HashMap 与 ConcurrentHashMap 在负数处理上的差异
HashMap 的负数处理
HashMap 通过 & (n - 1) 来计算桶索引,这一操作天然忽略了哈希值的符号位。由于桶数组的长度总是 2 的幂次方,位与运算可以自动将负数哈希值转化为有效的正整数索引。
这种设计非常简单高效,特别适合单线程环境,不需要额外的哈希值处理步骤。
ConcurrentHashMap 的负数处理
ConcurrentHashMap 需要通过 spread() 函数进行更复杂的哈希值处理,包括高低位混合和限制哈希值为正数。通过这些步骤,ConcurrentHashMap 确保了哈希值的均匀分布,减少了高并发场景下的哈希冲突。
由于哈希值可能生成负数,因此必须通过 & HASH_BITS 将哈希值转化为正数,确保哈希表操作的可靠性和性能。
为什么 ConcurrentHashMap 没能像 HashMap 那样自动处理负数?
HashMap 的设计足够简单,依赖位与运算自然而然地处理了负数问题,而不需要额外的哈希值转换。
ConcurrentHashMap 由于需要在高并发环境下提供更好的哈希分布和性能,采取了更复杂的哈希处理机制,最终导致需要显式地处理负数哈希值,确保哈希值为正数,避免索引计算中的不确定性。
两者的差异反映了它们在使用场景上的不同:HashMap 适合单线程环境的高效存取,而 ConcurrentHashMap 则致力于在并发访问下提供更高的性能和更安全的操作。

818

被折叠的 条评论
为什么被折叠?



