Collections.synchronizedList() 无法解决并发遍历问题,因其仅保证单个方法原子性,复合操作(如size()+get())、迭代及批量操作仍需手动同步,且不适用于Stream或强一致性场景。

为什么 Collections.synchronizedList() 不能直接解决并发遍历问题
它确实能让你的 ArrayList 或 LinkedList 的单个操作(如 add()、get()、set())变成线程安全的,但所有方法内部只是简单套了一层 synchronized(this)。这意味着:多个线程调用 size() 和 get(i) 是各自加锁、各自释放的,中间没有原子性保障。
常见错误现象:ConcurrentModificationException 或越界异常(IndexOutOfBoundsException),尤其在 for 循环里先 list.size() 再逐个 get(i) 时——因为另一个线程可能在两次调用之间删掉了最后一个元素。
- 别指望它能保护复合操作,比如“检查是否存在再添加”(
if (!list.contains(x)) list.add(x);)必须手动加锁 - 迭代器(
list.iterator())返回的对象本身不是同步的,遍历时必须手动同步外部对象:synchronized (list) { Iterator it = list.iterator(); while (it.hasNext()) foo(it.next()); } - 如果用增强 for 循环(
for (E e : list)),底层仍调用iterator(),同样需要显式同步块包裹
什么时候可以用 Collections.synchronizedList() 简单应付
适用于读多写少、且不涉及迭代或条件复合操作的场景,比如一个后台线程不断往列表里塞日志,多个监控线程只做 get(0) 或 size() 快照统计——只要不依赖多次调用间的逻辑一致性,它就足够轻量。
- 写入线程唯一,或写操作频率极低(例如配置加载后只追加一次)
- 读操作不要求强实时性,允许看到“上一时刻”的快照结果
- 没有
removeIf()、sort()、replaceAll()这类批量操作需求(它们不会被自动同步,需额外同步块) - 不与
Stream配合使用(list.parallelStream()会绕过同步逻辑,引发数据竞争)
Collections.synchronizedList() 和 CopyOnWriteArrayList 怎么选
两者都提供线程安全列表,但机制和适用边界完全不同:synchronizedList 是阻塞式、低内存开销、高争用下性能差;CopyOnWriteArrayList 是无锁读、写时复制、适合读远多于写的场景。
- 如果你的列表大小稳定在百以内,且写操作极少(比如监听器注册表),
CopyOnWriteArrayList更安全省心——它的迭代器天然弱一致性,不怕并发修改 - 如果列表很大(万级+),或写操作频繁(每秒多次),
CopyOnWriteArrayList的每次写都触发数组复制,GC 压力陡增,此时synchronizedList+ 外部控制仍是更实际的选择 -
synchronizedList包装的是任意List实现(包括你自己写的),而CopyOnWriteArrayList是具体类,无法包装已有列表
正确初始化和使用的最小实践模板
别只写 new ArrayList() 后随手包一层——初始容量、泛型擦除、是否允许 null,这些都要提前想清楚。
- 初始化时指定合理初始容量,避免频繁扩容带来的同步开销:
List<string> syncList = Collections.synchronizedList( new ArrayList(128));</string> - 如果用在类字段中,务必声明为
List接口类型,而非具体实现类,防止误调未同步的方法(如ArrayList.ensureCapacity()) - 对外暴露时,考虑返回
Collections.unmodifiableList()包装过的视图,避免下游无意中绕过同步逻辑 - 测试时别只测单线程逻辑;用
ExecutorService启多个线程反复add()+size(),观察是否出现size()返回负数或突变(说明同步失效或存在其他共享状态污染)
真正麻烦的从来不是加锁本身,而是那些你以为“已经锁住”、其实根本没覆盖到的代码路径——比如流式处理、lambda 引用、或者跨方法的状态判断。用 synchronizedList 前,先问自己:这段逻辑里,有没有任何地方依赖了两次方法调用之间的中间状态?










