ArrayList本身非线程安全,需通过Collections.synchronizedList包装实现基本安全;但遍历和复合操作必须手动同步,高并发场景应考虑CopyOnWriteArrayList或ReentrantLock替代。

Java 中 ArrayList 本身不是线程安全的,直接在多线程环境中读写会导致 ConcurrentModificationException、元素丢失、size 错误甚至数组越界。要让它“安全使用”,核心不是修改 ArrayList 本身,而是通过包装或替换方式提供线程安全的访问能力。最常用且标准的做法是用 Collections.synchronizedList() 包装,但必须理解它的边界和配套要求。
用 Collections.synchronizedList 包装(最常用)
这是 JDK 提供的轻量级、标准包装方式,本质是装饰器模式:返回一个代理 List,所有 public 方法内部都加了 synchronized(mutex) 锁。
- 创建方式简洁:
List<String> safeList = Collections.synchronizedList(new ArrayList<>()); - 单个操作如
add()、get()、remove()、size()都是原子的,无需额外同步 - 底层锁对象默认是包装器自身(或传入的 list 实例),多个线程竞争同一把锁,读读也互斥,适合并发不极端的场景
遍历时必须手动同步(关键易错点)
包装后的列表 iterator() 方法本身没加锁,for-each 或显式迭代器仍是“快速失败”机制。若遍历中其他线程修改结构,仍会抛异常。
- 正确写法是显式加锁,锁对象必须与包装器一致:
synchronized (safeList) { for (String s : safeList) { ... } } - 也可用原始 ArrayList 引用(如果还持有):
synchronized (originalList) { ... },效果等价 - 切勿省略同步块——这是面试和线上故障高频坑点
复合操作需整体加锁(不能依赖包装)
像“先检查再添加”、“获取并移除最后一个元素”这类逻辑,即使每个子操作安全,组合起来仍非原子。
立即学习“Java免费学习笔记(深入)”;
- 反例:
if (!safeList.contains(x)) safeList.add(x);→ 两个线程可能同时通过判断,导致重复添加 - 正解:把整个逻辑包进同步块:
synchronized (safeList) { if (!safeList.contains(x)) safeList.add(x); } - 类似地,
safeList.get(0); safeList.remove(0);必须合并加锁,否则中间可能被其他线程干扰
根据场景考虑更优替代方案
Collections.synchronizedList 是通用解法,但不是万能解法。高并发读多写少时,它因读读互斥而性能受限;写频繁时又可能成为瓶颈。
-
读远多于写(如配置项缓存、监听器列表)→ 选
CopyOnWriteArrayList:读无锁、迭代器绝对安全,但每次写都复制数组,内存和 CPU 开销大 -
需要更精细控制或复杂逻辑→ 手动用
ReentrantLock包裹 ArrayList,可实现读写分离、条件等待等高级并发控制 - 不推荐 Vector:虽线程安全,但所有方法用 synchronized 修饰,粒度粗、API 过时、性能比 synchronizedList 更差


















