ConcurrentHashMap中key为自定义对象时,若equals()与hashCode()违反契约,会导致查不到key或内存泄漏;根本原因是a.equals(b)为true时a.hashCode()≠b.hashCode(),或hashCode()不一致。

直接看核心问题:当 ConcurrentHashMap 中的 key 是自定义对象,且 equals() 和 hashCode() 重写不一致或不符合契约时,会出现两种典型现象——查不到本该存在的 key(逻辑丢失),或key 持续堆积无法被覆盖/清除(内存泄漏)。这不是并发问题,而是哈希契约破坏导致的基础行为异常。
确认是否真由 equals/hashCode 引发
先排除其他常见原因(如静态引用、未清理缓存),聚焦在“对象作为 key 的行为是否符合预期”:
- 用相同字段构造两个对象
a和b,验证a.equals(b)是否为true;再检查a.hashCode() == b.hashCode()是否成立——若不成立,就是根本原因 - 在
ConcurrentHashMap.put(k, v)后立即调用map.get(k),如果返回null,但k明显已存入,大概率是hashCode()不稳定或与equals()冲突 - 打印日志观察:同一业务场景下,反复创建相同语义的 key 对象,但每次
hashCode()返回值都不同(比如依赖了未初始化字段、随机数、时间戳等)
检查重写是否满足三大契约
Java 规范强制要求三个基本条件,缺一不可:
-
一致性:同一个对象在生命周期内多次调用
hashCode(),只要参与计算的字段没变,结果必须相同 -
对称性:若
a.equals(b)为true,则b.equals(a)也必须为true -
传递性与相等推导:若
a.equals(b) && b.equals(c),则a.equals(c)应为true;更重要的是——a.equals(b)为true⇒a.hashCode() == b.hashCode()
特别注意:如果用了 Lombok 的 @EqualsAndHashCode,要确认 exclude 或 of 指定的字段和 equals() 实际比较的字段完全一致;否则 IDE 自动生成和 Lombok 生成可能不统一。
立即学习“Java免费学习笔记(深入)”;
用工具辅助定位问题 key
不要靠肉眼猜,用代码快速暴露异常行为:
- 写一个临时校验方法,遍历当前 map 中所有 key,对每对 key 调用
equals()和hashCode()比较,记录所有违反契约的组合 - 在 key 类中重写
toString(),清晰输出参与比较的所有字段值,方便日志中比对“看似相同却查不到”的 key - 用
jcmd <pid> VM.native_memory summary</pid>或jmap -histo <pid></pid>查看 key 类实例数是否异常增长——如果数量随请求线性上升且不下降,说明 put 进去的 key 因 hash 不同被当成新 key 存储,老 key 又没被替换,就构成泄漏
修复后验证是否真正生效
改完代码不能只跑单元测试,要模拟真实使用路径:
- 构造一批语义相同但对象不同的 key(例如 new 两次相同 ID 的 User),依次
put,然后用其中一个去get——应能命中最后一次 put 的 value - 用 JMH 或简单循环压测,在高并发下反复
put+get+remove,观察 GC 后 map.size() 是否稳定,而不是持续增长 - 开启 JVM 参数
-XX:+PrintGCDetails配合堆直方图对比,确认 key 类实例不再“只增不减”
这类问题不复杂但容易忽略,关键在契约意识和验证闭环。


















