Collections.synchronizedList仅保证单个方法线程安全,不保证复合操作原子性;遍历等操作需显式synchronized(safeList)加锁,且必须统一锁对象,否则仍会并发修改异常或数据不一致。

为什么 Collections.synchronizedList 不能直接解决并发修改问题
它只保证单个方法调用(如 add()、get())是线程安全的,但不保证复合操作原子性。比如遍历 + 删除、检查后插入这类“先读再写”的逻辑,仍会抛 ConcurrentModificationException 或产生数据不一致。
常见错误现象:多线程遍历时突然崩溃,或某次 size() 返回值在循环中失效;看似加了同步,结果还是漏数据。
- 所有迭代必须手动加锁:
synchronized(list)包裹for-each或iterator() -
list.iterator()返回的迭代器本身**不是同步的**,不能依赖它自动同步 - 如果列表被传递给其他模块,对方可能无意中绕过同步块直接调用方法
什么时候该用 Collections.synchronizedList 而不是 CopyOnWriteArrayList
适用场景是读多写极少、且写操作基本不涉及遍历——比如日志缓冲区、配置监听器注册表。它的优势在于写操作不阻塞读,但每次写都要复制整个数组,内存和 GC 压力大。
Collections.synchronizedList 更适合写操作稍频繁、内存敏感、且你能控制访问模式的场景。
- 写操作频率 > 每秒几十次时,
CopyOnWriteArrayList复制开销明显上升 - 列表元素较大(如含 byte[] 的对象),复制成本高,优先选
synchronizedList - 需要严格保序且对迭代一致性有要求(
CopyOnWriteArrayList迭代的是快照,看不到最新写入)
正确使用 Collections.synchronizedList 的三步写法
核心原则:所有可能引发竞态的操作,必须共享同一把锁——即原始包装后的列表对象本身。
List<String> raw = new ArrayList<>(); List<String> safeList = Collections.synchronizedList(raw);
- 遍历必须显式同步:
synchronized(safeList) { for (String s : safeList) { ... } } - 条件插入需同步块包裹:
synchronized(safeList) { if (!safeList.contains(x)) safeList.add(x); } - 避免将
raw暴露出去——一旦外部直接操作raw,同步完全失效
容易被忽略的陷阱:包装后仍可能出错的边界情况
最隐蔽的问题是“锁对象被替换”或“同步粒度错配”。比如把 synchronizedList 存进 ConcurrentHashMap,然后只对 map 操作加锁,却忘了 list 自身操作仍需独立同步。
- 不要用
synchronized(new Object())替代synchronized(safeList)——锁对象不一致等于没锁 - 若列表作为参数传入工具方法,确保该方法内部也遵循同步约定,否则锁被绕过
- 子类化或代理该列表(如通过 Spring AOP)可能导致同步失效,因为
synchronizedList是装饰器模式,行为绑定在实例上
真正难处理的从来不是加锁动作本身,而是确认所有相关代码路径是否都落在同一把锁的保护下——这点比选容器类型更关键。

















