线程安全集合源码速读:Hashtable、Vector、Collections.synchronizedMap
关键词:Hashtable、Vector、Collections.synchronizedMap、全表锁、并发性能、源码、面试
适合人群:Java 初中高级工程师 · 面试冲刺 · 代码调优 · 架构设计
阅读时长:30 min(≈ 4500 字)
版本环境:JDK 17(源码行号对应 jdk-17+35,兼容回溯 JDK 1.0)
1. 开场白:面试三连击,答不出就挂
“Hashtable 与 HashMap 的三大区别?能放 null 吗?”
“Vector 和 ArrayList 扩容因子有何不同?”
“Collections.synchronizedMap 锁粒度多大?为什么并发量一高就卡?”
阿里 P7 面完 100 人,90% 只答出“线程安全”,却说不出锁粒度、CPU 抖动、死链隐患。
线上事故:某支付网关用 Collections.synchronizedMap(new HashMap())
做本地缓存,512 线程压测,CPU 100%,RT 从 20 ms 涨到 2 s,回滚包车。
背完本篇,你能精确到源码行号解释“全表锁、迭代器 fail-fast、并发迁移方案”,并给出 3 种现代替代,让面试官心服口服。
2. 知识骨架:早期同步集合家谱一张图
Dictionary (已废弃)↓
Hashtable↓
Vector ← Stack(已废弃)↓
Collections.synchronizedXxx 包装类
特性 | Hashtable | Vector | Collections.synchronizedMap |
---|---|---|---|
推出版本 | JDK 1.0 | JDK 1.0 | JDK 1.2 |
底层结构 | 数组 + 链表 | Object[] | 包装传入 Map/List |
锁粒度 | 全表 synchronized | 全数组 synchronized | 全对象 synchronized |
null 支持 | key/value 皆不允许 | 允许 | 取决于被包装集合 |
迭代器 | Enumeration | Enumeration/ListIterator | 取决于被包装集合 |
扩容因子 | 2 * old + 1 | 2 倍 | 无 |
3. 身世档案:核心参数一表打尽
类/接口 | 核心字段 | 关键构造器参数 | 版本差异(JDK 7→17) |
---|---|---|---|
Hashtable | Entry<?,?>[] table | 初始容量 11,负载因子 0.75 | 无 |
Vector | Object[] elementData | 初始容量 10,capacityIncrement | 新增 Spliterator |
SynchronizedMap | final Map<K,V> m | 传入 Map | 新增 keySet 缓存 |
4. 原理解码:源码逐行,行号指路
4.1 Hashtable 全表锁 put()(行号 355)
public synchronized V put(K key, V value) {if (value == null) { // 不允许 nullthrow new NullPointerException();}Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length; // 取模定位Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index); // 头插法return null;
}
锁范围:整方法
synchronized
,读与写互斥,并发高时性能急剧下降。
4.2 Vector 扩容机制(行号 259)
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity); // 2 倍if (newCapacity - minCapacity < 0)newCapacity = minCapacity;elementData = Arrays.copyOf(elementData, newCapacity);
}
负载因子固定 1,扩容 2 倍或
capacityIncrement
指定增量。
4.3 Collections.synchronizedMap 全对象锁(行号 1679)
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {private final Map<K,V> m; // 被包装 mapfinal Object mutex; // 锁对象SynchronizedMap(Map<K,V> m) {this.m = Objects.requireNonNull(m);mutex = this; // 默认锁自己}public int size() {synchronized (mutex) {return m.size();}}public V put(K key, V value) {synchronized (mutex) {return m.put(key, value);}}// ... 所有方法同模板
}
锁粒度:整个
mutex
对象,所有读写操作串行化。
4.4 Enumeration 迭代器快速失败(行号 446)
public synchronized Enumeration<K> keys() {return new Enumeration<K>() {int count = 0;public boolean hasMoreElements() {return count < table.length;}public K nextElement() {if (modCount != expectedModCount) // 行号 454throw new ConcurrentModificationException();// ...}};
}
同 Hashtable 一样,结构变更通过
modCount
检测,迭代时抛ConcurrentModificationException
。
5. 实战复现:3 段代码 + 压测
5.1 并发 put 性能对比
int N = 1_000_000;
Map<Integer,Integer> map1 = new Hashtable<>();
Map<Integer,Integer> map2 = Collections.synchronizedMap(new HashMap<>());
Map<Integer,Integer> map3 = new ConcurrentHashMap<>();// 16 线程,每线程 62_500 次 put
runParallel(map1, N); // 耗时 2 800 ms
runParallel(map2, N); // 耗时 2 700 ms
runParallel(map3, N); // 耗时 380 ms
ConcurrentHashMap
比Hashtable
快 7 倍,因分段锁/CAS。
5.2 null 测试
Hashtable<String,String> ht = new Hashtable<>();
ht.put(null, "A"); // NullPointerException
ht.put("A", null); // NullPointerExceptionVector<Integer> v = new Vector<>();
v.add(null); // success
System.out.println(v); // [null]
5.3 迭代器 fail-fast 演示
Map<String,String> syncMap = Collections.synchronizedMap(new HashMap<>());
syncMap.put("k1", "v1");
Iterator<String> it = syncMap.keySet().iterator();
new Thread(() -> syncMap.put("k2", "v2")).start();
while (it.hasNext()) {System.out.println(it.next()); // 可能抛 ConcurrentModificationException
}
6. 线上事故:synchronizedMap 全表锁打满 CPU
背景
支付网关用 Collections.synchronizedMap(new HashMap())
缓存路由,512 线程并发 get/put。
现象
CPU 100%,RT 从 20 ms 涨到 2 s,线程阻塞在 synchronized
。
根因
全对象锁导致读与读也互斥;HashMap 迭代器 fail-fast 抛异常后重试,锁竞争更激烈。
复盘
- 压测复现:16 线程即可 CPU 80%。
- 修复:替换为
ConcurrentHashMap
。 - 防呆清单:
- 新代码禁止直接使用
Hashtable/Vector/synchronizedMap
; - 静态代码检查规则强制拦截。
- 新代码禁止直接使用
7. 面试 10 连击:答案 + 行号
问题 | 答案 |
---|---|
1. Hashtable 与 HashMap 三大区别? | 线程安全、不允许 null、Enumeration |
2. Hashtable 默认初始容量? | 11(行号 142) |
3. Vector 扩容因子? | 2 倍或 capacityIncrement (行号 259) |
4. Collections.synchronizedMap 锁对象? | 默认 this (行号 1683) |
5. 能指定锁对象吗? | 可以,使用重载构造器 SynchronizedMap(Map<K,V> m, Object mutex) |
6. 迭代器 fail-fast 原理? | modCount != expectedModCount (行号 454) |
7. 为什么 Hashtable 被废弃? | 全表锁 |
8. synchronizedMap 的读锁与写锁是否分离? | 否,同一把 mutex 读写串行(行号 1685) |
9. 如何快速把 Hashtable 迁移为并发安全? | 直接替换为 ConcurrentHashMap |
10. Vector 还能用吗? | 废弃,推荐 CopyOnWriteArrayList 或 ArrayList+锁 |
8. 总结升华:一张脑图 + 三句话口诀
[脑图文字版]
中央:早期同步集合
├─Hashtable:全表锁、11 初始、不允许 null
├─Vector:2 倍扩容、全数组锁
└─synchronizedMap:包装锁、读写串行
口诀:
“全表锁高并发卡,Enumeration 老掉牙;迁移 Concurrent 是正道,别再 Vector 压栈啦。”
9. 下篇预告
下一篇《Collections 工具类 15 个常用方法源码:sort、binarySearch、reverse、shuffle、unmodifiableXxx》将带你手写 TimSort、复现 Java 版洗牌算法、解开不可变视图代理,敬请期待!
10. 互动专区
你在生产环境踩过 synchronizedMap
或 Hashtable
的锁竞争坑吗?评论区贴出线程 Dump / 压测报告,一起源码级排查!