Java Synchronized Collections为何抛ConcurrentModificationException及适用场景
Synchronized Collections 适用场景及问题解析
首先给出触发异常的代码:
import java.util.*; public class SynchFail { static List<Integer> LIST = new ArrayList<Integer>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { while (true) { LIST.add(1); } }}).start(); new Thread(new Runnable() { @Override public void run() { while (true) { List<Integer> syncList = Collections.synchronizedList(LIST); synchronized(syncList) { for (Integer thisInt : syncList) { } } } }}).start(); } }
运行正常的代码:
import java.util.*; public class SynchSucceed { static List<Integer> LIST = new ArrayList<Integer>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { while (true) { synchronized(LIST) { LIST.add(1); } } }}).start(); new Thread(new Runnable() { @Override public void run() { while (true) { synchronized(LIST) { for (Integer thisInt : LIST) { } } } }}).start(); } }
异常原因说明
ConcurrentModificationException的触发根源是读写线程的同步规则不一致:
- 第一段代码的写线程直接操作原始
ArrayList的add方法,无任何同步控制,随时会修改集合的modCount计数 - 读线程临时包装出的
synchronizedList实例的锁对写线程完全无效,写线程根本不会感知这把锁的存在,迭代过程中集合被修改就会触发fail-fast机制抛出异常
第二段代码所有读写操作都用同一把锁控制LIST的访问,所以不会出现迭代中途集合被修改的情况,运行正常。
Synchronized Collections 核心特性
Collections.synchronizedXXX系列包装的同步集合,仅保证单个方法(如add()/remove()/get())调用的原子性,对于迭代、批量操作、条件更新这类复合操作,不会自动为整个操作流程加锁,需要手动控制同步。
适用场景
- 业务以单个原子读写操作为主的场景:如果很少需要迭代、批量更新等复合操作,用同步集合可以避免手动给每个单操作加锁,代码更简洁
- 所有线程仅访问包装后同步集合实例的场景:确保没有任何线程直接操作被包装的原始集合,所有读写都走同一个同步集合实例,其内置锁才能对所有操作生效
- 并发量不高、对性能要求宽松的场景:同步集合用的是独占全量锁,读写互斥,并发度低。如果并发量不大,用它的开发成本比
CopyOnWriteArrayList、ConcurrentHashMap这类并发容器更低,逻辑也更简单 - 复合操作可主动手动加锁的场景:如果需要做迭代、“不存在则添加”这类复合操作,主动给同步集合实例加锁即可保证整个操作的原子性
注意事项
- 禁止同时操作包装后的同步集合和原始集合,否则锁规则会直接失效
- 迭代操作必须手动加锁,同步集合不会自动为迭代流程加锁,不加锁依然有
ConcurrentModificationException风险 - 所有读写线程必须遵守同一套锁规则,不能出现部分线程加锁、部分线程不加锁的情况
内容的提问来源于stack exchange,提问作者Dave Carpeneto
相关产品推荐
相关产品推荐

