一、HashMap
1. 实现原理
数据结构
- HashMap 的底层由数组 + 链表 + 红黑树组成 - 数组(哈希桶):初始容量为 16,存储链表头节点或红黑树根节点,每次扩容翻倍 
- 链表:解决哈希冲突,同一索引的键值对以链表形式存储 
- 红黑树(JDK 1.8+):链表长度超过 8 且数组容量 ≥ 64 时,链表转为红黑树,查询复杂度从 O(n) 优化为 O(log n) 
 
- 哈希函数与索引计算 - 哈希扰动:通过 hash(key) = (h = key.hashCode()) ^ (h >>> 16) 混合高低位,降低冲突概率 
- 索引定位:index = (n - 1) & hash(n 为数组长度且保持 2 的幂次方),位运算替代取模提升效率 
 
核心流程
- Put 操作: - 计算哈希值定位索引 
- 若桶为空则插入新节点,否则遍历链表/树: - 存在相同 key 时覆盖旧值(通过 equals 判断) 
- 不存在则插入链表末尾或红黑树 
 
- 触发扩容条件:元素数量 ≥ 容量 × 负载因子(默认 0.75) 
 
- Get 操作: - 计算哈希值定位索引,遍历链表/树匹配 key 
- 返回 value 或 null 
 
哈希冲突
- 解决机制 - 链地址法:冲突键值对存入同一桶的链表或红黑树 
- 扩容:负载因子超标时数组翻倍,重新散列元素以减少冲突 
 
线程安全
- 多线程同时修改结构(如 put/remove)可能导致数据不一致或死循环(JDK 1.7 头插法) 
- 解决方案:使用 ConcurrentHashMap 或 Collections.synchronizedMap 包装 
版本差异
- JDK 1.7:头插法链表,多线程扩容易引发死循环 
- JDK 1.8:尾插法,引入红黑树优化,降低并发风险 
自定义 Key
- 必须重写 hashCode() 和 equals():确保相同逻辑的 key 哈希值一致且等价,否则数据可能丢失 
- 示例:未重写时,内容相同的两个对象作为 key 会被视为不同,导致 get 返回 null 
性能优化建议
- 初始容量:预估数据量设置(如 new HashMap<>(1000)),避免频繁扩容。 
- 负载因子:内存充足时可降低负载因子以减少冲突。 
- 哈希分散性:优化 hashCode() 方法,使键分布均匀。 
详见 http://www.shj4u.work/itdocx/docs/java/core/collection/source-code-analysis/map/HashMap.html
2. 线程安全
结论
HashMap 不是线程安全的类,多线程环境下直接使用可能导致
- 数据丢失(多个线程 put 时覆盖写入) 
- 数据不一致(读取到中间状态) 
- 死循环(JDK 1.7 头插法扩容形成环形链表) 
- 迭代器失效(遍历时修改结构抛出 ConcurrentModificationException) 
核心原因
- 操作非原子性:put 和扩容涉及多步骤且未加锁 
- 共享资源竞争:多线程同时修改同一哈希桶(链表/红黑树) 
- JDK 1.7 头插法缺陷:扩容时链表顺序反转易形成环 
替代方案
- ConcurrentHashMap - JDK 1.7 使用分段锁,JDK 1.8+ 改用 CAS + synchronized 锁桶头节点 
- 高并发场景首选,性能优异 
 
- Collections.synchronizedMap - 通过 synchronized 包装 HashMap 所有方法 
- 简单但性能较差,适合低并发场景 
 
- Hashtable(已过时) - 全局锁机制导致性能极低,不推荐使用 
 
面试要点
- 明确结论:HashMap 非线程安全 
- 核心问题:数据竞争导致覆盖/丢失,JDK 1.7 死循环问题 
- 解决方案:优先推荐 ConcurrentHashMap,次选 SynchronizedMap 
- 版本差异:JDK 1.8 尾插法解决死循环,但数据竞争仍存在 
3. PUT 过程
哈希值计算
- 调用 key.hashCode() 获取原始哈希值,通过扰动函数 (h = key.hashCode()) ^ (h >>> 16) 混合高低位,降低冲突概率 
- 若 key 为 null,哈希值固定为 0 
定位桶索引
- 计算索引:index = (n - 1) & hash,其中 n 为数组长度(保持 2 的幂次方) 
- 若数组未初始化(首次插入),调用 resize() 初始化容量为 16,负载因子为 0.75 
哈希冲突处理
- 空桶:直接创建新节点存入数组 
- 非空桶 - 链表:遍历链表查找相同 key(equals 匹配) - 存在相同 key:覆盖旧 value 
- 不存在:插入链表末尾。链表长度 ≥ 8 且数组容量 ≥ 64 时转为红黑树 
 
- 红黑树:调用 putTreeVal() 插入或更新节点 
 
扩容触发与迁移
- 当元素数量超过阈值(容量 × 负载因子)时触发扩容,新容量为旧容量的 2 倍 
- 迁移规则:根据 (e.hash & oldCap) 判断节点分配到原索引或原索引 + 旧容量的位置 
返回结果与状态更新
- key 存在时返回旧 value,否则返回 null 
- 更新 modCount 计数器,用于迭代器的快速失败检测 
4. GET 过程
哈希值计算与索引定位
- 计算 key 的哈希值:key.hashCode() 经扰动函数 (h = key.hashCode()) ^ (h >>> 16) 混合高低位 
- 若 key 为 null,哈希值固定为 0 
- 计算索引:index = (n - 1) & hash(n 为数组长度,保持 2 的幂次方) 
桶位元素检查
- 根据索引定位数组中的桶,若桶为空(table[index] == null),直接返回 null 
- 若桶非空,检查首节点的哈希值和 key 是否匹配(通过 == 或 equals)。若匹配则返回 value 
链表或红黑树遍历
- 链表:首节点不匹配时遍历链表,通过 equals 方法逐一比对 key,找到则返回 value,否则返回 null 
- 红黑树(JDK 1.8+):调用 getTreeNode() 递归查找树节点,基于哈希值和 key 大小决定子树方向 
时间复杂度与性能
- 无冲突时 O(1),链表冲突时 O(n),红黑树优化后 O(log n) 
注意事项
- 必须正确重写 hashCode() 和 equals() 以确保 key 匹配 
- JDK 1.8 红黑树优化长链表查询效率 
- 迭代器依赖 modCount 检测并发修改,触发 ConcurrentModificationException 
5. GET 安全性
纯读场景的理论安全性
- 若所有线程仅执行 get() 且未发生扩容,理论上是线程安全的 
- 此时 get() 不修改结构,仅遍历现有节点 
并发写入的潜在风险
- 死循环(JDK 1.7):扩容时链表头插法可能形成环,get() 遍历陷入死循环 
- 中间状态读取:扩容迁移过程中可能读到未完成迁移的旧数组或部分数据 
- 数据不一致:并发 put() 导致覆盖写入或扩容未完成时 get() 返回 null 或旧值 
JDK 版本差异影响
- JDK 1.7:头插法扩容存在成环风险,get() 高并发下可能死循环 
- JDK 1.8+:尾插法避免成环,但 get() 仍可能读取到扩容中间状态 
安全实践建议
- 纯读场景:初始化后不修改 HashMap 可视为安全 
- 读写混合场景:使用 ConcurrentHashMap 或 Collections.synchronizedMap 包装 
- 预分配容量:减少扩容触发概率 
根本原因与验证
- 问题根源:HashMap 未对结构性修改(扩容)与读取操作做同步控制 
- 复现方法:压力测试工具模拟并发读写,观察 CPU 占用或异常抛出 
6. Key
一般用什么做 Key,为啥 String 适合做 Key 呢?
常用 Key 类型
- String:最常用,因其不可变性和哈希计算高效 
- Integer/Enum:基于数值的哈希值,简单高效 
- 自定义不可变类:需严格实现 hashCode() 和 equals() 
- 其他包装类(如 Long):适用于简单键值场景 
String 适合作为 Key 的核心原因
- 不可变性 - 创建后不可修改,哈希值稳定,避免数据丢失 
 
- 哈希计算高效 - hashCode() 使用 31 的质数乘法,分布均匀且结果缓存复用 
 
- 字符串常量池优化 - 相同字面量共享内存,加速比较(通过 == 判断引用相等性) 
 
- 完善的 equals() 实现 - 字符逐位比对,性能高效且逻辑严谨 
 
- 语义清晰 - 适用于配置、URL 等场景,易读且兼容性强 
 
使用 Key 的注意事项
- 优先选择不可变对象:可变对象可能导致哈希值变化,引发数据不一致 
- 正确重写 hashCode() 和 equals() 
- 确保逻辑一致,避免哈希表定位失败 
- 避免复杂对象:增加冲突概率且维护成本高 
HashMap key 可以为 null 吗?
允许 null 键的机制
- key 为 null 时,HashMap 通过 hash() 返回哈希值 0,避免空指针异常 
- null 键存储在数组索引 0 的位置,且仅允许存在一个(键唯一性保证) 
设计原因与场景
- 灵活性:支持表示“未知键”或配置中的默认值 
- 单线程优化:设计未考虑多线程下 null 键的二义性(如 get(key) 返回 null 时无法区分键是否存在) 
与其他容器的对比
- Hashtable:禁止 null 键/值,直接调用 key.hashCode() 会抛空指针异常 
- ConcurrentHashMap:禁止 null 键/值,因多线程下无法消除二义性风险,且实用价值有限 
使用注意事项
- 线程安全:多线程操作 null 键需同步控制,否则可能导致数据覆盖或逻辑错误 
- 代码可读性:建议用 Optional 类或明确业务语义替代,避免过度依赖 null 键 
7. 红黑树
为什么要用红黑树而不是平衡二叉树?
插入与删除性能优势
- 红黑树插入/删除最多需 2 次旋转,平衡二叉树(如 AVL 树)可能需多次旋转 
- 红黑树允许局部不平衡(最长路径 ≤ 2 倍最短路径),降低调整频率,适合高频更新场景 
查询效率的折中
平衡二叉树查询略优(树更平衡),但 HashMap 树规模小(阈值 8,树高 ≤ 3),红黑树 O(log n) 效率已接近理想值
计算开销与内存优化
红黑树通过颜色标记替代高度差计算,减少 CPU 开销。元素减少到 6 时转回链表,避免内存浪费
工程实现的成熟性
红黑树在 Linux 内核、Java TreeMap 等广泛应用,实现经过长期验证。平衡二叉树旋转逻辑复杂,维护成本高
综合性能平衡
红黑树在插入、删除、查询间平衡更优,适合 HashMap 动态存储需求(如扩容与数据迁移)
8. 多线程
在多线程下可能会出现的问题?
数据覆盖与丢失
多线程同时插入相同哈希桶时,可能因同时检测空桶导致数据覆盖。并发扩容时节点迁移遗漏可能造成数据丢失
死循环
- JDK 1.7 及之前:头插法扩容导致链表成环,触发 get() 或 put() 操作时陷入无限循环,CPU 占用率飙升 
- JDK 1.8 之后:树化扩容时,也会出现死循环常见,不过概率很低 
ConcurrentModificationException 异常
迭代器遍历期间其他线程修改结构,触发快速失败机制抛出异常
脏读与逻辑错误
读取到扩容迁移中的中间状态数据,get(key) 返回 null 时无法区分键是否存在或值为 null
扩容性能下降
多线程重复触发扩容导致哈希重计算和节点迁移,增加 CPU 负载
JDK 1.8 扩容时仍需锁定当前桶引发线程阻塞
结构破坏与内存泄漏
并发修改可能导致链表断裂或树结构失衡,节点引用错误可能引发内存无法回收
9. 扩容机制
- 扩容触发条件 - 元素数量超过阈值(阈值 = 容量 × 负载因子,默认 0.75) 
- 链表长度 ≥8 且数组长度 <64 时优先扩容而非树化 
 
- 扩容流程 - 新数组创建:容量翻倍(保持 2 的幂次方) 
- 数据迁移 - 链表拆分(JDK8+):采用高低位分组策略,将链表拆分为低位链(原索引)和高位链(原索引 + 旧容量) 
- 红黑树处理:节点数 ≤6 时退化为链表,否则拆分迁移 
- 尾插法优化:JDK8 用尾插法替代头插法,避免多线程成环问题 
 
 
- 性能优化 - 位运算 (n-1) & hash 替代取模运算,提升计算效率 
- 默认负载因子 0.75 平衡空间利用率与冲突概率 
- 初始化时建议预估容量(如 new HashMap<>(预估容量/0.75 +1)) 
 
- 多线程风险 - 并发插入或扩容可能导致数据覆盖、脏读 
- JDK7 头插法扩容会引发死循环,JDK8 尾插法彻底解决 
- 推荐使用 ConcurrentHashMap 保证线程安全 
 
- 参数调优与监控 - 强制禁用树化:通过 JVM 参数 -Djava.util.HashMap.treeifyThreshold=2147483647(牺牲查询性能) 
- 负载因子调整:内存敏感场景可调高(如 0.9),时间敏感场景调低(如 0.6) 
- 扩容性能监控:关注 GC 日志中 HashMap.resize() 耗时,优化容量规划 
 
存 20 个元素,会扩容几次?
- 初始容量与扩容触发条件 - 默认初始容量为 16,负载因子为 0.75,扩容阈值为 容量 × 0.75 
- 首次 put() 时初始化容量为 16(不视为扩容) 
- 当元素数量超过阈值(如 16 × 0.75 = 12)时触发扩容 
 
- 扩容过程分析(默认参数下存入 20 个元素) - 第一次扩容:存入第 13 个元素时触发,容量从 16 扩容至 32,新阈值更新为 32 × 0.75 = 24 
- 后续操作:存入第 14~20 个元素时,元素数量始终 ≤24,不再触发扩容 
 
- 特殊情况说明 - 链表树化:若某链表长度达到 8 但数组长度 <64,会优先扩容而非树化,但仅计入一次扩容 
- 手动指定容量:例如 new HashMap<>(32) 时,阈值 = 32 × 0.75 = 24,存入 20 个元素不会触发扩容 
 
- 总结 - 默认场景下存入 20 个元素仅触发 1 次扩容(容量 16 → 32) 
- 扩容次数由元素数量是否超过阈值决定,与链表/树结构无关 
 
10. 2的n次方
大小为什么是 2 的 n 次方大小呢?
- 位运算优化与计算效率 - 容量为 2 的 n 次方时,(n-1) & hash 等价于 hash % n,但位运算性能更优(避免除法运算开销) 
- 示例:当容量为 16(二进制 10000)时,n-1 = 15(二进制 1111),位运算直接取哈希值低 4 位 
 
- 哈希分布均匀性 - 低位掩码(n-1 全 1)确保哈希值的所有位参与索引计算,减少哈希碰撞概率 
- 若哈希值高位不同但低位相同,仍可能分配到同一桶,但掩码机制比非 2 幂次方容量更均衡 
 
- 扩容迁移高效性 - 新容量为旧容量 2 倍时,迁移只需判断哈希值最高有效位 
- 最高位为 0:新索引 = 原索引 
- 最高位为 1:新索引 = 原索引 + 旧容量 
- 示例:旧容量 16,哈希值 10101 的索引从 5 变为 21(原索引 + 16) 
 
- 容量自动对齐机制 - 通过 tableSizeFor() 方法将用户输入容量对齐到最小 2 的幂次方。例如输入 21 时调整为 32 
- 实现方式:通过位右移和或操作填充最高位后的所有位为 1,再加 1 得到目标容量 
 
- 数据结构兼容性 - 链表转红黑树、拆分迁移等操作依赖容量为 2 的幂次方,例如高低位拆分逻辑与位掩码计算直接关联 
- 容量非 2 的幂次方时,扩容后无法保证高低位拆分规则的简洁性,增加迁移复杂度 
 
11. 负载因子
- 定义与作用 - 负载因子是 HashMap 中元素数量与容量的比率,用于控制扩容时机 
- 当元素数量超过 容量 × 负载因子 时触发扩容(容量翻倍并 rehash) 
 
- 默认值 0.75 的权衡 - 空间与时间平衡: - 过高(如 1.0)导致哈希冲突增多,查询效率降低 
- 过低(如 0.5)导致扩容频繁,空间利用率下降 
 
- 实践验证:0.75 是多数场景下减少冲突与避免频繁扩容的最优解 
 
- 调整策略 - 高并发场景:降低负载因子(0.5~0.6),减少冲突提升查询效率 
- 内存敏感场景:提高负载因子(0.8~0.9),减少扩容频率但容忍更高冲突 
- 初始化优化:通过构造函数指定(如 new HashMap<>(16, 0.6f)),或按 预估容量 / 负载因子 +1 计算初始容量 
 
- 对性能的影响 - 查询效率:负载因子越高,链表/红黑树遍历时间可能增加 
- 扩容成本:负载因子越低,rehash 操作越频繁,CPU 消耗增加 
 
- 设计依据 - 源码注释明确 0.75 是空间利用率与时间复杂度的折衷选择 
- 扩容机制(如 (n-1) & hash)需与负载因子设计兼容 
 
二、HashTable
1. 实现原理
- 底层数据结构 - 基于数组 + 链表实现,数组元素为 Entry 对象(包含 key、value、hash 和 next 指针) 
- 哈希函数与索引计算 
- 哈希值计算:key.hashCode(),再通过 (hash & 0x7FFFFFFF) % 数组长度 确定索引 
- 0x7FFFFFFF 确保哈希值为非负数,避免索引为负 
 
- 线程安全实现 - 所有公共方法(如 put、get)添加 synchronized 关键字,保证单线程操作 
- 高并发性能差,推荐改用 ConcurrentHashMap 
 
- 扩容机制 - 初始容量 11,负载因子 0.75,阈值 = 容量 × 负载因子 
- 触发扩容后,新容量 = 旧容量 × 2 + 1,并重新哈希所有元素 
 
- 哈希冲突处理 - 链地址法:冲突的 Entry 对象以链表形式存储,新元素插入链表头部(JDK 1.8 前) 
- 查找时遍历链表,时间复杂度 O(n) 
 
- 特殊限制 - key 和 value 均不允许为 null,否则抛出 NullPointerException 
- 继承自 Dictionary 类(已过时),与 HashMap(继承 AbstractMap)不同 
 
2. 线程安全
- 方法级同步锁 - 所有公共方法(如 put、get)使用 synchronized 修饰,确保同一时间仅一个线程操作 HashTable 
 
- 内存可见性保障 - synchronized 保证锁释放时将本地修改刷新到主内存,获取锁时读取最新值,避免数据不可见问题 
 
- 扩容机制 - 初始容量 11,负载因子 0.75,扩容阈值为 容量 × 负载因子。触发扩容后,新容量 = 旧容量 × 2 +1 
 
- 与其他容器的对比 - ConcurrentHashMap:分段锁或 CAS 机制支持并发读写,性能更高 
- synchronizedMap:对普通 HashMap 加对象级锁,性能与 HashTable 类似 
 
- 使用限制与性能问题 - 禁止 null 键/值,操作需显式处理空值 
- 全表锁导致读写互斥,高并发场景性能差,推荐改用 ConcurrentHashMap 
 
三、ConcurrentHashMap
怎么实现的?
1. 实现机制
- 线程安全机制 - JDK 1.7 分段锁:将哈希表划分为多个 Segment,每个段独立加锁,支持不同段并发操作 
- JDK 1.8 细粒度锁 + CAS:取消分段锁,对哈希桶(Node)加锁。插入时先尝试 CAS 无锁插入头节点,冲突时用 synchronized 加锁处理链表或红黑树。 
 
- 数据结构与扩容 - 数组 + 链表/红黑树:链表长度 ≥8 且数组容量 ≥64 时转为红黑树,查询效率提升至 O(log n) 
- 渐进式扩容:容量翻倍,多线程协作迁移数据,迁移期间读写可同时访问新旧数组。用 ForwardingNode 标记已迁移的桶 
 
- CAS 与原子操作 - 无锁初始化:通过 CAS 确保单线程初始化哈希表 
- 原子更新控制:插入空桶时 CAS 写入头节点,扩容时通过 sizeCtl 变量协调线程任务分配 
 
- 内存可见性保障 - 哈希桶数组声明为 volatile,确保读取最新状态 
- 节点 next 字段使用 volatile 修饰,防止链表遍历时数据不一致 
 
- 性能对比 - 相比 Hashtable:ConcurrentHashMap 支持并发读写不同桶,读操作无锁(JDK 1.8),吞吐量更高 
- 相比 synchronizedMap:细粒度锁减少竞争,支持迭代期间并发修改 
 
2. 锁
ConcurrentHashMap 用了悲观锁还是乐观锁?
- JDK 1.7 分段锁(悲观锁) - 基于 ReentrantLock 实现,每个 Segment 独立加锁 
- 写操作需获取分段独占锁,同一分段内线程竞争阻塞,不同分段可并行操作 
 
- JDK 1.8 混合锁机制 - CAS(乐观锁) - 初始化数组、插入空桶头节点时使用 CAS 无锁操作 
- 扩容时通过 CAS 更新 sizeCtl 协调多线程任务 
 
- synchronized(轻量级悲观锁) - 哈希冲突时(链表/红黑树操作)对当前桶加锁,锁粒度细化到单个桶 
- 读操作无锁:依赖 volatile 修饰的 Node.val 和 next 字段保证可见性 
 
 
- 锁选择逻辑与优势 - 低冲突场景(如空桶插入)优先使用 CAS,避免线程阻塞 
- 高冲突场景(如链表修改)使用 synchronized 保证数据一致性 
- 混合机制减少上下文切换和自旋开销,提升并发性能 
 
四、分段锁
分段锁怎么加锁的?
1. 底层实现
- 定位分段与锁对象 - 键的哈希值经过两次散列运算 - 第一次散列定位到具体的 Segment 分段(如 hash % concurrencyLevel,默认并发级别 16) 
- 第二次散列在 Segment 内部定位到具体的 HashEntry 桶(链表头节点位置) 
 
- 每个 Segment 继承自 ReentrantLock,包含独立的 HashEntry 数组和锁状态 
 
- 获取分段锁流程 - 写操作(如 put)调用 Segment.lock() 获取该分段的可重入锁 
- 不同分段的操作并行执行(如操作 Segment[0] 和 Segment[3] 互不阻塞) 
- 若分段被占用,当前线程阻塞直到锁释放 
 
- 操作与释放锁 - 加锁后在 Segment 的 HashEntry 链表中执行插入/删除/更新操作 
- 操作完成后调用 Segment.unlock() 释放锁,其他线程可竞争该锁 
 
- 并发性能优势 - 不同分段操作完全并行,仅同一分段内触发锁竞争 
- 读操作无锁,依赖 volatile 修饰的 HashEntry 字段(如 value)保证可见性 
 
2. 可重入性
分段锁是可重入的吗?
- 实现 - JDK 1.7 的 Segment 锁继承自 ReentrantLock,具备可重入特性 
- 内部维护计数器(state 变量):同一线程多次获取锁时计数器递增,释放时递减,归零后释放资源 
 
- 原理 - 线程持有标记:ReentrantLock 记录当前持有锁的线程,同一线程重复获取时直接累加计数器 
- 释放规则:每次 lock() 必须对应一次 unlock(),否则锁无法完全释放 
 
- 非可重入锁 - 非可重入锁(如简单互斥锁)无法识别持有者,同一线程重复获取会死锁 
- 分段锁的可重入性避免此问题,支持递归或嵌套调用 
 
- 实际意义 - 避免死锁:例如 put 操作内部调用其他需加锁的逻辑时,线程可重入同一锁 
- 简化编程:无需避免同一线程的重复加锁行为,降低代码复杂度 
 
五、HashMap vs HashTable
HashMap 和 HashTable 有什么不一样的?HashMap 一般怎么用?
1. 核心差异
- 线程安全 - HashTable 所有方法使用 synchronized 修饰,性能低 
- HashMap 非线程安全,高并发下推荐 ConcurrentHashMap 
 
- null 支持 - HashTable 禁止 null 键/值,否则抛出异常 
- HashMap 允许一个 null 键和多个 null 值 
 
- 继承结构 - HashTable 继承 Dictionary 类(已过时) 
- HashMap 继承 AbstractMap,实现 Map 接口 
 
- 性能与扩容 - HashTable 初始容量 11,扩容公式为 旧容量 × 2 +1 
- HashMap 初始容量 16,翻倍扩容,哈希桶 + 链表/红黑树结构(JDK 1.8+) 
 
- 迭代器 - HashTable 使用 Enumerator,不支持快速失败 
- HashMap 使用 Iterator,支持快速失败机制 
 
2. HashMap用法
- 基本操作 
HashMap<String, Integer> map = new HashMap<>();
map.put("key", 100);      // 添加键值对
int value = map.get("key"); // 获取值
map.remove("key");        // 删除键值对- 遍历方式 
// entrySet() 遍历键值对
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}
// keySet() 或 values() 单独遍历键或值- 嵌套结构 
HashMap<String, HashMap<String, Integer>> nestedMap = new HashMap<>();
nestedMap.put("A", new HashMap<>()).put("B", 200);- 性能优化 - 初始化时预估容量:new HashMap<>(预期容量/0.75 +1) 
- 确保键对象正确实现 hashCode() 和 equals() 
 
- 适用场景 - 缓存系统(快速存取) 
- 配置管理(键值映射) 
- 数据统计(如词频统计) 
 
3. 总结建议
- 单线程优先选择 HashMap,高并发用 ConcurrentHashMap 
- 避免滥用 null 键/值,提高代码健壮性 
- 合理初始化容量以减少扩容开销 
六、synchronized vs CAS
已经用了 synchronized,为什么还要用 CAS 呢?
- 性能优化与场景适配 - synchronized 的阻塞代价:线程获取不到锁时会阻塞,导致上下文切换和内核态切换,高并发下吞吐量下降 
- CAS 的无锁优势:在低冲突场景(如空桶插入)通过自旋重试避免阻塞,性能更高 
 
- 锁粒度与并发效率 - synchronized 的粗粒度问题:锁定整个对象或代码块,强制串行化所有操作 
- CAS 的细粒度控制:针对单个变量原子操作(如 AtomicInteger 计数更新),不影响其他变量或操作 
 
- 功能互补性 - CAS 的局限性:无法处理复杂逻辑(如链表遍历+修改)或多变量操作,需 synchronized 保证结构一致性 
- synchronized 的优化:Java 1.6 后引入偏向锁、轻量级锁,但 CAS 可减少锁升级概率 
 
- 混合锁机制的实践 - 协同使用场景: - 低冲突优先 CAS(如空桶插入) 
- 高冲突退化为 synchronized(如链表/红黑树修改) 
- 设计哲学:乐观并发控制(CAS)与悲观锁(synchronized)结合,平衡性能与一致性 
 
 
七、HashMap & HashTable & ConcurrentHashMap
- 线程安全与锁机制 - HashTable:使用 synchronized 锁所有方法,锁粒度粗(整表锁定),多线程性能差 
- HashMap:非线程安全,单线程性能最优,多线程操作可能导致数据不一致或死循环 
- ConcurrentHashMap:JDK 1.7 分段锁,JDK 1.8 CAS + synchronized 细粒度锁(锁定单个桶或链表节点),高并发性能优 
 
- null 键值支持 - HashTable:禁止 null 键/值,插入 null 抛出 NullPointerException 
- HashMap:允许一个 null 键和多个 null 值 
- ConcurrentHashMap:禁止 null 键/值,避免并发下语义歧义(如无法区分“键不存在”与“值为 null”) 
 
- 性能对比 - 单线程:HashMap > ConcurrentHashMap > HashTable 
- 多线程:ConcurrentHashMap 无锁读 + 细粒度写锁,性能远高于 HashTable 
 
- 数据结构与冲突处理 - HashTable:数组 + 链表,无红黑树优化,冲突链表可能过长 
- HashMap:JDK 1.8+ 数组 + 链表/红黑树(链表长度 ≥8 且数组容量 ≥64 时树化) 
- ConcurrentHashMap:同 HashMap,但线程安全机制保证并发操作 
 
- 迭代器行为 - HashTable:强一致性,迭代时修改抛 ConcurrentModificationException 
- HashMap:fail-fast 快速失败,多线程修改可能抛异常 
- ConcurrentHashMap:弱一致性,允许迭代期间修改,不保证实时数据 
 
- 适用场景 - HashTable:已过时,仅用于遗留系统或低并发场景 
- HashMap:单线程场景(如本地缓存、配置存储) 
- ConcurrentHashMap:高并发场景(如分布式缓存、计数器) 
 
 
             
           
             
                         
             
            
评论区