您现在的位置: 365建站网 > 365文章 > Collections.synchronizedMap 实战:遍历加锁、原子操作与选型对比

Collections.synchronizedMap 实战:遍历加锁、原子操作与选型对比

文章来源:365jz.com 点击数:11 更新时间:2025-10-12 02:33

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

Java线程安全映射Collections.synchronizedMap实战

一、它到底做了什么

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 的对比与选型

维度synchronizedMapConcurrentHashMap
锁粒度整表一把锁分段/CAS,读无锁
高并发吞吐低(读写互斥)高
遍历必须手动加锁,弱一致需自己保证迭代器弱一致,无需加锁、不抛 CME
null 键值取决于底层 Map,HashMap 允许不允许 null 键/值
原子复合操作只有 putIfAbsent/compute 等默认方法提供 merge/compute/computeIfAbsent 等高性能原子方法

选型结论:需要高并发就用 ConcurrentHashMap;synchronizedMap 主要用于两种场景——① 需要保序(包 LinkedHashMap/TreeMap)且并发不高;② 老代码改造时最小改动加一层保护。如果需要“线程安全 + 有序”,可考虑 Collections.synchronizedSortedMap(new TreeMap<>()) 或 ConcurrentSkipListMap(后者并发更好)。

四、几个易忽略细节

  1. toString、equals、hashCode 也都被同步;但把 sync 传给第三方库时,对方内部的遍历年不一定加锁。
  2. 包装是“视图”而非拷贝:日志里 System.out.println(sync) 会持锁,可能因大表长时间阻塞别人。
  3. 多个相关 Map 需要原子更新(如双向映射),可用同一个外部 mutex 一起锁,保证跨集合一致性。
  4. 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建站器 »