ArrayList
是java.util
包中的一个类,它不是线程安全的。如果多个线程同时对同一个ArrayList
进行操作,可能会导致并发问题,如数据不一致或ConcurrentModificationException
异常。
1. 场景复现
1.1 数据不一致问题示例代码
import java.util.ArrayList;
import java.util.List;
public class ArrayListConcurrencyExample {
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
// 创建并启动多个线程,同时向ArrayList添加元素
Runnable addTask = () -> {
for (int i = 0; i < 1000; i++) {
arrayList.add(i);
}
};
Thread thread1 = new Thread(addTask);
Thread thread2 = new Thread(addTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出ArrayList的大小,不一定是预期的 2000
System.out.println("Size of arrayList: " + arrayList.size());
}
}
1.2 ConcurrentModificationException 问题示例代码
ConcurrentModificationException
通常会在迭代ArrayList
(或其他集合)的同时对其进行结构性修改时抛出。
import java.util.ArrayList;
import java.util.List;
import java.util.Iterator;
public class ConcurrentModificationExample {
public static void main(String[] args) {
List<String> arrayList = new ArrayList<>();
arrayList.add("Item1");
arrayList.add("Item2");
arrayList.add("Item3");
// 获取迭代器
Iterator<String> iterator = arrayList.iterator();
// 开始迭代
while (iterator.hasNext()) {
String item = iterator.next();
System.out.println(item);
// 在迭代过程中尝试修改ArrayList的结构,会引发ConcurrentModificationException
if (item.equals("Item2")) {
arrayList.remove(item);
}
}
}
}
当处理ArrayList
的并发问题时,不同的方法有不同的细节和适用场景。以下是对每种方法的详细解释:
2. 解决并发的三种方法
2.1 使用 Collections.synchronizedList
使用 Collections.synchronizedList
创建线程安全的ArrayList
这是一种简单的方式来使ArrayList
线程安全。它实际上是包装了一个原始的ArrayList
,并在每个方法上添加synchronized
关键字来确保每个方法在同一时间只能由一个线程访问。
List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
这种方法适用于那些多数情况下是读操作,但偶尔需要写操作的情况。请注意,尽管每个方法都是线程安全的,但多个操作之间没有原子性保证,因此还需要其他方式来确保多个操作的一致性。
例如下面的代码就会出现并发问题:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class SynchronizedListExample {
public static void main(String[] args) {
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Runnable addAndRemoveTask = () -> {
for (int i = 0; i < 1000; i++) {
synchronizedList.add(i);
synchronizedList.remove(synchronizedList.size() - 1);
}
};
Thread thread1 = new Thread(addAndRemoveTask);
Thread thread2 = new Thread(addAndRemoveTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Size of synchronizedList: " + synchronizedList.size());
}
}
在这个示例中,两个线程同时执行add
和remove
操作。虽然每个操作本身是线程安全的,但它们的组合会导致竞态条件,多次运行后,会出现下面的情况:
- 最终列表的大小可能不是预期的
2000
。 - 由于两个线程同时进行
remove
操作,可能导致其中一个线程试图删除一个元素,但在另一个线程之前已经删除了,导致IndexOutOfBoundsException
异常或其他不一致的结果
Exception in thread "Thread-0" java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:248)
at java.base/java.util.Objects.checkIndex(Objects.java:372)
at java.base/java.util.ArrayList.remove(ArrayList.java:536)
at java.base/java.util.Collections$SynchronizedList.remove(Collections.java:2435)
at com.test.testlist.SynchronizedListExample.lambda$main$0(SynchronizedListExample.java:14)
at java.base/java.lang.Thread.run(Thread.java:834)
Size of synchronizedList: 1
这突显了Collections.synchronizedList
在某些情况下可能无法提供足够的并发保护,因此需要额外的同步措施或选择更适合并发操作的数据结构。
2.2 使用 CopyOnWriteArrayList(推荐使用)
CopyOnWriteArrayList
是一种并发集合,它通过在写操作时创建一个新的副本来解决并发问题。这意味着读操作不会受到写操作的影响,而且不会抛出ConcurrentModificationException
异常。
List<String> list = new CopyOnWriteArrayList<>();
这种方法适用于读操作频繁,写操作较少的情况,因为写操作会比较昂贵。但它非常适用于多线程下的读操作,因为它不需要额外的同步。
2.3 使用显式的同步控制
这种方法需要在需要修改ArrayList
的地方使用synchronized
块或锁来确保线程安全。这是一种更精细的控制方法,适用于需要更多控制和协同操作的场景。
List<String> list = new ArrayList<>();
// 在需要修改list的地方加锁
synchronized (list) {
list.add("item");
}
这种方式要求手动管理锁,通过加锁确保在修改ArrayList
时进行同步,以防止多个线程同时访问它。
3. 总结
- 一般在日常编码中,直接使用
CopyOnWriteArrayList
就能满足很多场景; 但是由于每次进行写操作时,都需要复制整个列表,这会导致写操作的性能较低,尤其在列表很大时。因此,CopyOnWriteArrayList
适用于读操作频繁、写操作较少的场景。 - 使用
CopyOnWriteArrayList
时候,应该避免在迭代过程中修改列表;CopyOnWriteArrayList
的迭代器具有弱一致性,在迭代过程中,迭代器可能无法反映出最新的修改,可能会遗漏或重复元素。如果非要强一致性,那就需要全局锁或分布式锁来处理了。 - 大多数场景中,更多的还是读多写少; 所以一般解决并发的方法,其实就是让并发写的操作,变成串行的;如果非要保证最终的强一致性,那肯定最终还是串行化处理,非常影响性能。
- 如果是分布式系统的话,那肯定就要使用分布式锁来处理了。