ArrayList线程不安全问题及解决方案详解
问题背景
在多线程编程中,我们经常会遇到集合类的线程安全问题。Java中的ArrayList
是一个常用的集合类,但它不是线程安全的。当多个线程同时操作同一个ArrayList
实例时,可能会出现各种不可预料的问题。
问题演示
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {new Thread(() -> {list.add(UUID.randomUUID().toString().substring(0, 8));System.out.println(list);}, String.valueOf(i)).start();
}
运行上述代码,很可能会遇到以下异常:
ArrayIndexOutOfBoundsException
:数组越界异常ConcurrentModificationException
:并发修改异常或者出现数据不一致的情况
为什么ArrayList线程不安全?
ArrayList
的线程不安全主要体现在以下几个方面:
add方法非原子操作:
add()
操作涉及多个步骤(检查容量、扩容、赋值),在多线程环境下可能被中断modCount计数器:迭代过程中如果结构被修改,会抛出
ConcurrentModificationException
可见性问题:一个线程的修改可能不会立即对其他线程可见
解决方案
方案一:使用Vector类
List<String> list = new Vector<>();
Vector
是Java早期提供的线程安全集合类,通过在方法上添加synchronized
关键字实现同步。
优点:
简单易用,直接替换即可
保证强一致性
缺点:
性能较差,所有操作都需要获取锁
过于保守的同步策略
方案二:使用Collections.synchronizedList()
List<String> list = Collections.synchronizedList(new ArrayList<>());
这种方法返回一个同步包装器,将所有方法用synchronized
块包装。
优点:
灵活性高,可以包装任意List实现
与Vector类似的线程安全性
缺点:
性能仍然有损耗
迭代时需要手动同步
// 迭代时需要额外同步
synchronized(list) {Iterator<String> it = list.iterator();while (it.hasNext()) {// 处理元素}
}
方案三:使用CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList
是JUC包中提供的线程安全集合,采用"写时复制"策略。
工作原理:
读操作:无锁,直接访问当前数组
写操作:加锁,复制原数组,在新数组上修改,最后替换引用
优点:
读操作性能极高,适合读多写少的场景
不会抛出
ConcurrentModificationException
缺点:
写操作性能较差,需要复制整个数组
内存占用较大
数据弱一致性(读操作可能看不到最新的修改)
性能对比
实现方式 | 读性能 | 写性能 | 一致性 | 内存占用 |
---|---|---|---|---|
ArrayList | 高 | 高 | 无保证 | 低 |
Vector | 低 | 低 | 强一致 | 低 |
Collections.synchronizedList | 低 | 低 | 强一致 | 低 |
CopyOnWriteArrayList | 极高 | 低 | 弱一致 | 高 |
实际应用建议
读多写少的场景:优先考虑
CopyOnWriteArrayList
写多读少的场景:考虑使用
Collections.synchronizedList()
或Vector
高并发且需要高性能的场景:考虑使用并发容器如
ConcurrentHashMap
(对应List可以考虑分段锁实现)单线程环境:直接使用
ArrayList
即可
总结
ArrayList
的线程安全问题在多线程环境下必须重视。根据实际应用场景选择合适的线程安全方案至关重要:
需要强一致性且不关心性能:
Vector
或Collections.synchronizedList()
读多写少且可以接受弱一致性:
CopyOnWriteArrayList
高性能要求:考虑自定义同步策略或使用更专业的并发容器
正确选择线程安全集合类,可以有效避免多线程环境下的各种诡异问题,提高程序的稳定性和性能。
注意:即使使用了线程安全的集合类,复合操作(如检查再添加)仍然可能需要额外的同步措施,这点需要特别注意。