Collections.synchronizedMap 实战:遍历加锁、原子操作与选型对比
Java 里 Collections.synchronizedMap 是最古老的线程安全 Map 包装器:它把任意 Map 的每个方法用同一把锁同步。听起来简单,实际使用时最容易踩的是遍历仍需手动加锁这一条。本文说明它的实现机制、与 ConcurrentHashMap 的取舍,以及正确使用姿势。

一、它到底做了什么
Map<String, Integer> base = new HashMap<>();
Map<String, Integer> sync = Collections.synchronizedMap(base);
返回的对象是 SynchronizedMap 包装类,内部每个方法都写成:
public V put(K key, V value) {
synchronized (mutex) { return m.put(key, value); }
}
要点:
- 锁是同一个对象
mutex,默认就是这个包装 Map 自身;也可用synchronizedMap(map, mutex)指定外部锁对象,便于多集合共用一把锁。 - 它是全表一把锁,读写互斥:一个线程在 put,其他线程 get 也会被挡住。
- 底层 Map 若被别的线程直接引用修改,包装类无法保护——所以包装后不要再把 base 暴露出去。
二、最大的坑:遍历和复合操作必须自己加锁
put/get 单个调用是原子的,但“先判断再插入”和遍历不是。Javadoc 明确要求:
// 错误:遍历时其他线程修改会抛 ConcurrentModificationException
for (Map.Entry<String, Integer> e : sync.entrySet()) {
System.out.println(e);
}
// 正确:迭代/遍历要手动锁同一个 mutex
synchronized (sync) {
for (Map.Entry<String, Integer> e : sync.entrySet()) {
System.out.println(e);
}
}
// 或者用快照,避免长时间持锁
new ArrayList<>(sync.entrySet()).forEach(...);
同理,if (!map.containsKey(k)) map.put(k, v); 这种“检查再执行”在并发下会重复写入,应改用 putIfAbsent(Java 8 起 Map 的默认方法,SynchronizedMap 也支持且原子)。
三、与 ConcurrentHashMap 的对比与选型
| 维度 | synchronizedMap | ConcurrentHashMap |
|---|---|---|
| 锁粒度 | 整表一把锁 | 分段/CAS,读无锁 |
| 高并发吞吐 | 低(读写互斥) | 高 |
| 遍历 | 必须手动加锁,弱一致需自己保证 | 迭代器弱一致,无需加锁、不抛 CME |
| null 键值 | 取决于底层 Map,HashMap 允许 | 不允许 null 键/值 |
| 原子复合操作 | 只有 putIfAbsent/compute 等默认方法 | 提供 merge/compute/computeIfAbsent 等高性能原子方法 |
选型结论:需要高并发就用 ConcurrentHashMap;synchronizedMap 主要用于两种场景——① 需要保序(包 LinkedHashMap/TreeMap)且并发不高;② 老代码改造时最小改动加一层保护。如果需要“线程安全 + 有序”,可考虑 Collections.synchronizedSortedMap(new TreeMap<>()) 或 ConcurrentSkipListMap(后者并发更好)。
四、几个易忽略细节
- toString、equals、hashCode 也都被同步;但把
sync传给第三方库时,对方内部的遍历年不一定加锁。 - 包装是“视图”而非拷贝:日志里
System.out.println(sync)会持锁,可能因大表长时间阻塞别人。 - 多个相关 Map 需要原子更新(如双向映射),可用同一个外部 mutex 一起锁,保证跨集合一致性。
- Java 9 起还有
CopyOnWriteArrayList思路的 Map 版本吗?没有官方 COW Map;读极多写极少的场景可用Map.copyOf+ volatile 引用整体替换。
五、示例:安全计数器该怎么写
// 错误:get+put 非原子,会丢更新
Integer c = sync.get(k); sync.put(k, c == null ? 1 : c + 1);
// 推荐 1:用原子方法
sync.compute(k, (key, old) -> old == null ? 1 : old + 1);
// 推荐 2(高并发计数首选)
ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();
map.computeIfAbsent(k, x -> new LongAdder()).increment();
六、常见问题 FAQ
问:加了 synchronizedMap 还报 ConcurrentModificationException?
答:因为遍历处没加锁。包装类只保护单次方法调用。
问:Collections.unmodifiableMap 是不是也能线程安全?
答:不可变视图只读、本身线程安全,但它不阻止别人修改原始 Map,语义与 synchronizedMap 完全不同。
问:性能差多少?
答:读多写少高并发时差距可达数量级,因为 ConcurrentHashMap 读几乎无竞争,而 synchronizedMap 每次读都要抢锁。
核心记住:① 单方法调用才安全,遍历和“检查再执行”要自己 synchronized (sync);② 原子操作优先用 compute/putIfAbsent/merge;③ 高并发场景直接换 ConcurrentHashMap,只在建需要排序的共享 Map 时才用 synchronizedMap。
生产内容,同时打通搜索引擎(SEO)与AI搜索引擎(GEO)流量入口的软件! 点击进入365建站器 »
