Java 集合进阶:HashMap 源码剖析与线程安全指南
前言
在上一篇博客中,我们理清了集合框架的家族关系。但在面试中,面试官往往会针对 HashMap 的内部细节进行“连环炮”式发问。接下来带大家拆解这些硬核知识点,让你在面试中不仅能答对,还能答出深度。
一、 HashMap 的核心秘密:哈希碰撞与扩容
1. 为什么 HashMap 的长度要是 2 的幂次方?
这是为了性能优化。 计算元素在数组中位置的公式是 (n - 1) & hash(n 为数组长度)。
-
原因 1: 当 n 为 2 的幂次方时,
(n - 1)的二进制全为 1(如 16-1=15,二进制为 1111)。进行&位运算时,效果等同于取模(%),但位运算的速度远快于取模。 -
原因 2: 能使数据均匀分布,减少哈希冲突。
2. 负载因子(Load Factor)为什么是 0.75?
-
权衡的结果:如果负载因子太小(如 0.5),数组空位多,浪费空间;如果太大(如 1.0),冲突概率激增,查询效率下降。
-
0.75 是在时间和空间成本上的一个绝佳折中。
二、 线程安全集合:从被淘汰到主流
1. 被遗忘的古董:Hashtable 与 Vector
它们通过给每个方法加 synchronized 关键字来实现线程安全。
-
缺点:锁的粒度太粗。只要有一个人在修改,其他人连读都不行。就像全班共用一支笔,效率极低,现在已经基本被弃用。
2. 临时救兵:Collections.synchronizedXxx
可以通过 Collections.synchronizedMap(new HashMap<>()) 包装一个线程安全的集合。其本质还是加了全局锁,性能提升有限。
3. 当红炸子鸡:ConcurrentHashMap
它是多线程环境下的首选。
-
JDK 1.7:采用 分段锁(Segment),把大数组切成 16 段,每段一把锁。
-
JDK 1.8:抛弃分段锁,直接采用 CAS + synchronized。锁的粒度细化到了每一个数组桶(Node),只要两个线程操作的不是同一个位置,就不会产生竞争。
三、 CopyOnWriteArrayList:读写分离的智慧
当你需要在多线程环境下使用 ArrayList,且读多写少时,推荐使用 CopyOnWriteArrayList。
-
原理:写操作时,不直接修改原数组,而是把旧数组复制出一份,在副本上修改,改完后再将原引用指向新数组。
-
优点:读取时完全不用加锁,性能极高。
-
缺点:内存占用双倍,且数据具有“弱一致性”(改完的一瞬间,读取的人可能还在看旧副本)。
Java
// 写操作源码核心思想
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1); // 复制一个副本
es[len] = e; // 修改副本
setArray(es); // 引用切向新副本
return true;
}
}
四、 常见面试连环炮(避坑指南)
Q1:HashMap 和 HashSet 的区别?
-
HashMap实现了Map接口,存储键值对;HashSet实现了Set接口,仅存储对象。 -
HashSet的底层其实就是HashMap,它把存进去的值当作HashMap的 Key。
Q2:ConcurrentHashMap 的 get 方法需要加锁吗?
不需要。 它的 Node 节点的 val 和 next 都用了 volatile 修饰,保证了可见性。这使得读操作性能极佳。
Q3:为什么 ConcurrentHashMap 不允许插入 null?
为了避免二义性。在并发环境下,如果 get(key) 返回 null,你无法分辨是“这个 Key 对应的值就是 null”,还是“这个 Key 根本不存在”。因为在判断的间隙,其他线程可能已经修改了数据。
五、 总结与建议
作为后端开发,如果你在处理高并发业务,请记住以下选择策略:
-
单线程或局部变量:永远首选
HashMap/ArrayList。 -
多线程环境:
-
追求高性能 KV 存储 ->
ConcurrentHashMap。 -
读多写少且数据量小 ->
CopyOnWriteArrayList。
-
-
注意点:集合操作时,尽量预估大小,减少扩容带来的 CPU 抖动。
结语: 源码虽然枯燥,但它是通往高级工程师的必经之路。理解了底层,你写的每一行代码都会变得更有底气。
如果这篇文章对你有启发,欢迎转发给一起备战的小伙伴!
更多推荐



所有评论(0)