沉默是金,总会发光
大家好,我是沉默
在 Java 开发中,ArrayList 无疑是最常用的集合类之一。尽管它线程不安全,但在实际项目中,我们几乎无时无刻不在用它。
为什么?它的线程不安全问题到底有多严重?为什么开发者依然偏爱它?
今天,我们将从 技术原理、业务场景 和 最佳实践 三个层面,深度解析这个看似简单的技术问题背后的深层逻辑。
**-**01-
ArrayList 线程不安全的技术原理
1 线程安全问题的核心根源:缺乏同步机制
ArrayList 是基于动态数组实现的。它的核心方法(如 add()、remove())并没有加上 synchronized 关键字,这意味着,当多个线程同时操作同一个 ArrayList 实例时,数据一致性就会面临严重威胁。
代码示例:多线程环境下的元素添加
public class ArrayListThreadIssueDemo {
private static List<String> list = new ArrayList<>();
public static void main(String[] args) {
// 创建100个线程,每个线程向list中添加1000个元素
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
list.add("element"); // 多线程无同步保护
}
}).start();
}
try {
Thread.sleep(1000); // 等待线程执行完毕
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("预期大小:100000,实际大小:" + list.size());
}
}
问题:最终的 list.size() 可能小于预期的 100,000,原因是多线程并发操作没有同步,导致元素丢失或数据不一致。
2 扩容机制引发的线程安全问题
当 ArrayList 的元素数量超过当前容量时,它会自动触发扩容。在扩容过程中,ArrayList 会进行数组复制和底层数组引用更新。如果两个线程同时触发扩容,可能导致某个线程的扩容结果被覆盖,进而丢失数据。
扩容机制的风险
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量是原容量的1.5倍
elementData = Arrays.copyOf(elementData, newCapacity); // 复制元素到新数组
}
多线程环境下,扩容可能出现的问题是:若两个线程同时执行扩容操作,可能导致其中一个线程的修改结果被覆盖,丢失数据。
3 迭代器与 modCount 机制的冲突
ArrayList 使用 modCount 来记录集合的修改次数。在多线程环境下,如果一个线程在迭代过程中修改了集合,就会抛出 ConcurrentModificationException。
示例:迭代器与 modCount 的冲突
private class Itr implements Iterator<E> {
int expectedModCount = modCount; // 记录修改次数
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification(); // 检查修改次数是否一致
}
final void checkForComodification() {
if (modCount != expectedModCount) // 若修改次数变化,抛出异常
throw new ConcurrentModificationException();
}
}
**-**02-
为何在实际开发中广泛使用 ArrayList?
1 单线程环境:99%的业务场景
大多数情况下,ArrayList 是在单线程环境中使用的,比如:
-
Web 请求处理:Controller 层在单个线程内处理数据(如将查询结果放入
ArrayList)。 -
方法内部逻辑:在某个方法内创建并操作
ArrayList,方法执行完即销毁,不涉及多线程共享。
这种情况下,ArrayList 的线程不安全特性根本不会暴露,性能优势自然得到了发挥。
2 性能优势:与线程安全集合的差距
与线程安全的 Vector 相比,ArrayList 在单线程环境下的性能要高得多。无锁设计避免了同步带来的性能损耗。
-
操作效率:
ArrayList在单线程下,元素的添加操作是 O(1)(平均),而Vector因为加了锁,性能明显下降。 -
遍历效率:使用普通的
for循环遍历ArrayList性能远高于Vector的Enumeration迭代器。
3 多线程环境下的成熟解决方案
当确实需要在多线程环境中使用集合时,Java 提供了多种线程安全的方案,无需强制使用 Vector:
- 同步包装器:通过
Collections.synchronizedList()将ArrayList包装成线程安全的集合。
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
synchronized (safeList) { // 手动同步
safeList.add("element");
}
- 写时复制容器:
CopyOnWriteArrayList适用于读多写少的场景,写操作时会复制数组,保证线程安全。
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("write");
for (String s : cowList) { // 读操作直接访问原数组
process(s);
}
- 自定义锁范围:通过手动控制锁的粒度,平衡安全性和性能。
private static final Object LOCK = new Object();
public static void safeAdd(String element) {
synchronized (LOCK) { // 在操作期间加锁
list.add(element);
}
}
**-**03-
核心代码中的安全与效率平衡
1 复现线程安全问题的完整代码
public class ArrayListThreadUnsafeDemo {
private static List<String> list = new ArrayList<>();
private static final int THREAD_COUNT = 100; // 线程数
private static final int ELEMENT_PER_THREAD = 1000; // 每线程添加的元素数
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int j = 0; j < ELEMENT_PER_THREAD; j++) {
list.add(UUID.randomUUID().toString()); // 模拟多线程并发写
}
} finally {
latch.countDown();
}
}).start();
}
latch.await(); // 等待线程结束
System.out.println("预期元素总数:" + (THREAD_COUNT * ELEMENT_PER_THREAD));
System.out.println("实际元素总数:" + list.size()); // 多线程导致元素丢失
}
}
2 安全使用 ArrayList 的最佳实践
同步包装器
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
public static void addElement(String element) {
synchronized (safeList) { // 手动对列表加锁
safeList.add(element);
}
}
CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("write once");
for (String s : cowList) {
System.out.println(s); // 高效且线程安全的读取
}
**-**04-
总结
ArrayList 的广泛使用,不仅仅是因为它性能优越,还因为它在单线程环境下的稳定性和简单性。对于大多数业务场景,没有必要使用线程安全的集合,而是可以在适当场景下使用 ArrayList 以获得更高的性能。
然而,当涉及到多线程时,Java 提供了多种成熟的工具,如同步包装器、写时复制容器等,帮助开发者根据业务需求灵活选择最适合的方案。
通过理解 ArrayList 的线程安全问题,我们能够更加理性地选择合适的工具和解决方案,而不是盲目避开它。这也体现了软件开发中的一个原则:没有完美的工具,只有最适合的解决方案。
热门文章
**-**05-
粉丝福利
我这里创建一个程序员成长&副业交流群,和一群志同道合的小伙伴,一起聚焦自身发展,可以聊:技术成长与职业规划,分享路线图、面试经验和效率工具,探讨多种副业变现路径,从写作课程到私活接单,主题活动、打卡挑战和项目组队,让志同道合的伙伴互帮互助、共同进步。如果你对这个特别的群,感兴趣的,可以加一下,微信通过后会拉你入群,但是任何人在群里打任何广告,都会被我T掉。