Java 集合进阶:ConcurrentHashMap、HashSet、LinkedHashMap、TreeMap 和 fail-fast 一篇讲清 上一篇我们梳理了 Java 集合框架的基础包括 ArrayList、LinkedList、HashMap 的底层结构和扩容机制。这一篇继续往下看更高频的内容为什么 HashMap 线程不安全多线程下为什么推荐 ConcurrentHashMapJDK 7 和 JDK 8 的 ConcurrentHashMap 有什么区别HashSet 底层是什么为什么重写 equals 必须重写 hashCodeLinkedHashMap 和 TreeMap 适合什么场景什么是 fail-fast遍历集合时怎么安全删除元素这些问题在 Java 后端面试里非常常见尤其是 ConcurrentHashMap基本属于集合并发部分的核心。一、为什么 HashMap 线程不安全HashMap 没有同步控制。多线程同时操作 HashMap 时可能出现数据覆盖 size 统计不准 扩容时数据异常 读取到不一致数据比如两个线程同时往同一个桶里 put 数据可能一个线程的结果覆盖另一个线程。所以多线程共享 Map 时不应该直接使用 HashMap。单线程普通 key-value 场景用 HashMap 没问题多线程并发读写时就要换成线程安全的实现。二、多线程下应该用什么 Map常用ConcurrentHashMap它是线程安全的并且并发性能比 Hashtable 和 Collections.synchronizedMap() 更好。原因是它不是简单地给整个 Map 加一把大锁而是尽量减小锁粒度。可以这样记HashMap单线程普通场景 ConcurrentHashMap多线程并发场景三、ConcurrentHashMap 和 Hashtable 有什么区别对比ConcurrentHashMapHashtable线程安全是是锁粒度较细整个方法加锁性能更好较差null key/value不允许不允许是否推荐推荐基本不推荐Hashtable 很老很多方法直接加了 synchronized锁粒度粗并发性能差。现在多线程 Map 基本优先考虑 ConcurrentHashMap。四、JDK 7 的 ConcurrentHashMap 怎么实现JDK 7 使用的是Segment 分段锁结构大致是ConcurrentHashMap├── Segment 1├── Segment 2├── Segment 3└── Segment 4每个 Segment 类似一个小的 HashMap。不同线程操作不同 Segment 时可以并发执行。也就是说JDK 7 的核心是分段锁降低锁冲突图示如下五、JDK 8 的 ConcurrentHashMap 怎么实现JDK 8 放弃了 Segment 分段锁改为数组 链表 红黑树 CAS synchronized它的结构和 JDK 8 的 HashMap 类似但在并发控制上做了处理。put 时大致逻辑桶为空CAS 插入 桶不为空锁住桶头节点 synchronized也就是说它只锁当前桶不锁整个 Map。流程图这就是 JDK 8 ConcurrentHashMap 并发性能好的关键读操作基本无锁 写操作只锁局部桶六、ConcurrentHashMap 为什么用 synchronized不用 ReentrantLockJDK 8 中synchronized 已经做了很多优化比如偏向锁 轻量级锁 锁膨胀虽然后续 JDK 对锁实现又有调整但总体来说synchronized 已经不是早期那种“很慢”的印象了。另外使用 synchronized 锁住桶头节点有几个好处代码更简单 内存开销更低 JVM 可以持续优化所以 JDK 8 的 ConcurrentHashMap 选择了 CAS synchronized 的方式。七、ConcurrentHashMap 的 get 需要加锁吗通常不需要。get 主要依赖volatileCAS内存可见性设计它可以在不加锁的情况下读取数据。这也是 ConcurrentHashMap 并发性能好的原因之一读操作基本无锁 写操作只锁局部桶如果读多写少ConcurrentHashMap 的优势会更加明显。八、ConcurrentHashMap 为什么不允许 null key 和 null value因为多线程环境下null 会带来歧义。比如map.get(name) null这可能表示两种情况1. key 不存在 2. key 存在但 value 就是 null在并发环境下这种判断会很混乱。所以 ConcurrentHashMap 直接禁止 null key 和 null value。这样当 get 返回 null 时就可以明确表示这个 key 不存在九、HashMap 为什么允许 nullHashMap 是普通的非线程安全 Map主要用于单线程场景。它允许一个 null key 多个 null valuenull key 会放在某个固定位置通常可以理解为数组下标 0 的桶里。但多线程的 ConcurrentHashMap 为了避免并发歧义不允许 null。十、HashSet 底层是什么HashSet 底层其实是 HashMap。你添加元素set.add(A);底层类似map.put(A, PRESENT);其中 A 是 keyPRESENT 是一个固定的 Object 对象。所以 HashSet 去重本质上依赖的是HashMap 的 key 不重复十一、HashSet 如何判断元素重复依赖两个方法hashCode() equals()判断流程先比较 hashCode 如果 hashCode 不同认为不是同一个元素 如果 hashCode 相同再用 equals 比较流程图所以自定义对象放入 HashSet 时必须正确重写equals() hashCode()否则去重可能失效。十二、为什么重写 equals 必须重写 hashCode因为 HashSet、HashMap 会先用 hashCode 定位桶再用 equals 判断是否相等。如果两个对象 equals 相等但 hashCode 不同它们可能被放到不同桶里。这样 HashSet 就会认为它们不是重复元素。规则一定要记住equals 相等hashCode 必须相等hashCode 相等equals 不一定相等示例class User { private Long id; private String name; // 如果只重写 equals不重写 hashCode // 放入 HashSet 时可能无法正确去重 }十三、LinkedHashMap 有什么特点LinkedHashMap 继承自 HashMap但额外维护了一条双向链表。它可以保持顺序插入顺序 访问顺序常见用途需要按插入顺序遍历 Map 实现 LRU 缓存比如你希望遍历结果和插入顺序一致就可以使用 LinkedHashMap。如果开启访问顺序模式每次访问元素后会把它移动到链表尾部这就可以用来实现简单 LRU 缓存。十四、TreeMap 底层是什么TreeMap 底层是红黑树。特点key 有序 增删查时间复杂度 O(logN) 默认按 key 的自然顺序排序也可以传入自定义比较器MapInteger, String map new TreeMap((a, b) - b - a);如果需要排序的 Map可以使用 TreeMap。十五、HashMap、LinkedHashMap、TreeMap 怎么选可以这样记类型适合场景HashMap普通 key-value追求查询性能LinkedHashMap需要保持插入顺序或访问顺序TreeMap需要按照 key 排序ConcurrentHashMap多线程并发场景面试回答时最好结合场景说而不是只背概念。比如如果只是普通查询用 HashMap 如果遍历时要保持插入顺序用 LinkedHashMap 如果 key 要自动排序用 TreeMap 如果多线程并发读写用 ConcurrentHashMap。十六、TreeSet 底层是什么TreeSet 底层是 TreeMap。TreeSet 的元素会作为 TreeMap 的 key。所以 TreeSet 具有不重复 自动排序如果自定义对象放入 TreeSet需要实现 Comparable 或传入 Comparator否则对象之间无法比较大小可能会报错。十七、什么是 fail-fastfail-fast 是 Java 集合的一种快速失败机制。当你遍历集合时如果直接修改集合结构可能抛出ConcurrentModificationException例如for (String s : list) { if (a.equals(s)) { list.remove(s); } }这类代码通常会出问题。十八、为什么遍历集合时删除元素会报错增强 for 底层使用的是 Iterator。Iterator 会记录一个期望修改次数expectedModCount集合本身有一个实际修改次数modCount如果遍历过程中直接调用集合的 removemodCount 变了但 expectedModCount 没有同步更新。Iterator 发现两者不一致就抛出异常。流程如下十九、遍历时如何安全删除元素方式一使用 Iterator 自己的 remove 方法。IteratorString iterator list.iterator(); while (iterator.hasNext()) { String s iterator.next(); if (a.equals(s)) { iterator.remove(); } }方式二使用 Java 8 的 removeIf。list.removeIf(s - a.equals(s));不要在增强 for 里直接list.remove(s);二十、这一组怎么串起来讲可以这样回答HashMap 适合单线程普通 key-value 场景但线程不安全。多线程下应该使用 ConcurrentHashMap。JDK 7 的 ConcurrentHashMap 使用 Segment 分段锁JDK 8 改为数组 链表 红黑树并通过 CAS 和 synchronized 保证并发安全。 它的 get 通常不加锁put 时桶为空用 CAS桶不为空锁住桶头节点所以并发性能更好。HashSet 底层是 HashMap通过 key 去重所以自定义对象要重写 equals 和 hashCode。LinkedHashMap 可以维护插入顺序或访问顺序TreeMap 基于红黑树实现 key 排序。遍历集合时不能直接修改结构否则可能触发 fail-fast应该使用 Iterator.remove 或 removeIf。总体流程图总结这一组可以按下面这条线来记HashMap 线程不安全多线程用 ConcurrentHashMap。JDK 7 ConcurrentHashMap 是 Segment 分段锁。JDK 8 是 CAS synchronized锁粒度到桶。 ConcurrentHashMap 的 get 通常不加锁。ConcurrentHashMap 不允许 null是为了避免并发环境下的歧义。HashSet 底层是 HashMap去重依赖 equals 和 hashCode。 equals 相等hashCode 必须相等hashCode 相等equals 不一定相等。LinkedHashMap 可以保持插入顺序或访问顺序。TreeMap 基于红黑树按 key 排序。 TreeSet 底层是 TreeMap。fail-fast 是遍历时检测并发修改的机制。安全删除用 Iterator.remove 或 removeIf。这一组重点背ConcurrentHashMap JDK7/JDK8 区别、CAS synchronized、get 不加锁、为什么不允许 null、HashSet 底层、equals/hashCode、LinkedHashMap、TreeMap、fail-fast 码字不易技术干货深度复盘如果这篇文章帮你看清了 MyBatis-Plus 查询的底层底细别忘了 点赞、关注、收藏 三连走一波支持作者不迷路更多底层源码干货持续输出中让我们一起学习面试知识拿到自己想要的offer