






















HashMap 和 ConcurrentHashMap 是 Java 中用于存储键值对(Key-Value)的哈希表实现,但前者是非线程安全的,后者是线程安全的,且两者的底层原理和设计目标有显著差异。以下从数据结构、核心机制、线程安全(仅 ConcurrentHashMap) 三个维度详细解析。
HashMap 是 Java 中最常用的哈希表实现,用于快速存储和查询键值对,核心目标是高效的增删改查,不保证线程安全。
JDK 1.8 及之后,HashMap 的底层结构为 “数组(哈希桶)+ 链表 + 红黑树” 的复合结构:
Node[] table):数组的每个元素称为 “桶”,用于存储键值对的首节点。数组长度(容量,capacity)默认是 16,且始终保持为 2 的幂次方(如 16、32、64...),这是为了通过位运算快速计算元素位置。Node 节点):当多个键的哈希值映射到同一个桶时,会形成链表(解决哈希冲突)。每个 Node 包含 hash(键的哈希值)、key、value、next(下一个节点引用)。TreeNode 节点):当链表长度超过阈值(默认 8),且数组容量 ≥ 64 时,链表会转为红黑树(一种自平衡二叉查找树)。红黑树的查询时间复杂度为 O (log n),远优于链表的 O (n),可优化大量哈希冲突时的查询效率。HashMap 通过键(Key)的哈希值确定元素在数组中的位置,步骤如下:
int hash = hash(key),其中 hash(key) 是对 key.hashCode() 的二次哈希(通过高位异或低位,减少哈希冲突):
static final int hash(Object key) {
int h;
int index = (n - 1) & hash,其中 n 是数组容量(table.length)。由于 n 是 2 的幂次方,n-1 的二进制全为 1(如 16-1=15 → 1111),& 运算等价于取模(hash % n),但效率更高。put(key, value) 用于插入键值对,步骤如下:
table 未初始化或长度为 0,先触发扩容(resize())。key 的 hash 和 index,检查索引 index 处的桶是否为空:
Node 插入(table[index] = newNode(hash, key, value, null))。key 相同(hash 相等且 key.equals() 为 true),直接替换 value。TreeNode),调用树的插入方法(putTreeVal)。key,替换 value;treeifyBin)。size)超过阈值(threshold = capacity * loadFactor,默认负载因子 loadFactor=0.75),触发扩容(resize())。当元素数量超过阈值时,HashMap 会扩容(容量翻倍),目的是减少哈希冲突,步骤如下:
newCap = oldCap << 1(原容量 × 2,保持 2 的幂次方)。newTable),容量为 newCap。oldTable)中的元素迁移到新数组:
newCap 是原容量的 2 倍,新索引要么是原索引,要么是 原索引 + oldCap,无需重新计算 hash,通过 hash & oldCap 判断高位是否为 1 即可)。table 为 newTable,更新阈值(newThr = newCap * loadFactor)。HashMap 未做任何同步处理,多线程环境下操作可能导致问题:
put 时,可能覆盖彼此的插入结果。size 计数未加锁,可能导致统计结果错误。ConcurrentHashMap 是线程安全的哈希表实现,专为多线程环境设计,支持高并发的增删改查,同时尽量减少锁竞争带来的性能损耗。其设计在 JDK 1.7 和 1.8 有较大差异,这里以更常用的 JDK 1.8+ 为主讲解。
JDK 1.8 摒弃了 JDK 1.7 的 “分段锁(Segment)” 机制,底层结构与 HashMap 类似:“数组(哈希桶)+ 链表 + 红黑树”,但通过更细粒度的同步机制保证线程安全。
JDK 1.8 采用 “无锁 CAS 尝试 + synchronized 细粒度锁” 保证线程安全,避免了 JDK 1.7 分段锁的粗粒度竞争:
table[index] == null),通过 CAS 原子操作尝试插入新节点(casTabAt 方法),避免加锁。table[index])加 synchronized 锁,仅锁定当前冲突的桶,其他桶仍可被并发访问,锁粒度更细。put(key, value) 步骤与 HashMap 类似,但增加了同步处理:
key 的 hash(与 HashMap 相同),若数组未初始化,先通过 CAS 初始化(initTable)。index,检查桶是否为空:
casTabAt 方法(CAS)尝试插入新节点,成功则返回;失败(被其他线程抢先插入)则重试。MOVED 状态(正在扩容),当前线程协助扩容(helpTransfer),避免扩容线程负载过重。synchronized 锁,遍历链表 / 红黑树:
key,替换 value(通过 cas 保证原子性)。size(通过 addCount 方法,CAS 累加,超过阈值则触发扩容)。ConcurrentHashMap 的扩容支持多线程协同,避免单线程扩容效率低:
size 超过阈值时,由某个线程发起扩容(transfer 方法),将数组容量翻倍。oldTab)被分为多个段,每个线程负责迁移一段(通过 stride 控制,默认每个线程处理 16 个桶)。put/get 时,若发现正在扩容(桶的首节点为 MOVED),会协助迁移当前桶的元素,加速扩容过程。HashMap 类似(新索引 = 原索引 或 原索引 + oldCap),但通过 CAS 标记迁移状态,避免冲突。get(key) 操作无需加锁,直接通过哈希定位桶,遍历链表 / 红黑树查找,原因是:
value 用 volatile 修饰,保证多线程间的可见性(修改后立即刷新到主内存,读取时从主内存加载)。synchronized 锁保证原子性,读取时无需锁即可获取最新结构。| 维度 | JDK 1.7 ConcurrentHashMap | JDK 1.8 ConcurrentHashMap |
|---|---|---|
| 特性 | HashMap | ConcurrentHashMap |
|---|---|---|
实际开发中,单线程环境用 HashMap,多线程环境(如并发写入)用 ConcurrentHashMap。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。