🌺个人主页:杨永杰825_Spring,Mysql,多线程-CSDN博客
🎉相关链接:ArrayList介绍-CSDN博客
⭐每日一句:成为架构师路途遥远📢欢迎大家:关注🔍**+点赞👍+评论📝+**收藏⭐️
前言
- ArrayList是线程不安全的数据结构,这意味着当多个线程同时访问或修改ArrayList时,可能会导致数据一致性的问题。
- 当多个线程同时对ArrayList进行写操作(例如添加、删除、修改元素),可能会导致其中一个线程的操作被覆盖或丢失。这是因为ArrayList在进行修改操作时并没有进行同步处理,因此多个线程之间的操作顺序是不确定的。
不安全案例
下面是一个示例,展示了当多个线程同时访问一个ArrayList时可能出现的线程不安全问题:
import java.util.ArrayList;
import java.util.List;
public class ArrayListThreadUnsafeExample {
public static void main(String[] args) {
// 创建一个空的ArrayList
List<Integer> arrayList = new ArrayList<>();
// 创建两个线程,同时向ArrayList中添加元素
Thread thread1 = new Thread(new AddElementTask(arrayList));
Thread thread2 = new Thread(new AddElementTask(arrayList));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ArrayList size: " + arrayList.size());
}
// 添加元素的任务
static class AddElementTask implements Runnable {
private List<Integer> arrayList;
public AddElementTask(List<Integer> arrayList) {
this.arrayList = arrayList;
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
// 向ArrayList中添加元素
arrayList.add(i);
}
}
}
}
结果:
在上述示例中,我们创建了一个空的ArrayList,并启动两个线程分别向该ArrayList中添加1000个元素。由于ArrayList是线程不安全的,因此可能出现以下两种情况之一:
- 在添加元素的过程中,其中一个线程的操作被覆盖或丢失,导致最终ArrayList的大小小于2000。
- 在添加元素的过程中,两个线程同时修改ArrayList,导致其中一个线程的操作被覆盖或丢失,最终ArrayList的大小可能大于2000。
因此,使用多个线程同时对ArrayList进行操作时,不能保证最终结果的正确性和一致性。为了解决这个问题,可以使用线程安全的替代类(如Vector或CopyOnWriteArrayList)或者使用同步机制来保证线程安全。
CopyOnWriteArrayList
CopyOnWriteArrayList是Java集合框架中的一个线程安全的List实现类。它通过对底层数组进行复制来实现线程安全,因此在迭代操作时,不需要进行额外的同步措施。
特点
- 线程安全:CopyOnWriteArrayList通过加锁的方式实现线程安全。在多线程环境下,可以同时进行读操作,只有在写操作时才需要进行加锁。
- 读写分离:CopyOnWriteArrayList通过对底层数组进行复制,在写操作时复制一份新的数组,读操作则直接在原数组上进行。这样做的好处是,读操作不需要加锁,不会阻塞其他读操作,提高了并发性能。但是写操作需要加锁,并且写操作期间对原数组的读操作会被阻塞。
- 弱一致性:由于CopyOnWriteArrayList在写操作时是复制一份新的数组,因此在写操作期间,读操作可能会读取到旧的数据。所以CopyOnWriteArrayList提供的是弱一致性的读写一致性。
- 适合读多写少的场景:CopyOnWriteArrayList适用于读多写少的场景,例如:日志系统、缓存等。
常用方法
- add(E e):向列表末尾添加元素。
- remove(int index):移除指定索引处的元素。
- get(int index):获取指定索引处的元素。
- size():获取列表的大小。
总结:CopyOnWriteArrayList是一个线程安全的List实现类,通过读写分离和对底层数组进行复制来实现线程安全。它适用于读多写少的场景,并且提供了弱一致性的读写一致性。
案例
以下是一个示例代码,展示了如何使用CopyOnWriteArrayList类:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
// 创建一个CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 向列表中添加元素
list.add("A");
list.add("B");
list.add("C");
// 创建一个线程,在迭代过程中添加元素
Thread thread = new Thread(() -> {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element); // 输出当前迭代的元素
if (element.equals("B")) {
list.add("D"); // 在迭代过程中添加元素
}
}
});
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list);
}
}
在上述示例中,我们创建了一个CopyOnWriteArrayList并向其添加了三个元素。然后我们创建了一个线程,在迭代CopyOnWriteArrayList的过程中添加了一个新元素。CopyOnWriteArrayList是线程安全的,因此在迭代过程中添加元素不会导致ConcurrentModificationException异常。
输出结果可能是:
A
B
C
[A, B, C, D]
可以看到,即使在迭代过程中添加了元素,最终的CopyOnWriteArrayList仍然包含了新添加的元素。这是因为CopyOnWriteArrayList在添加和修改操作时,会创建一个新的拷贝并进行操作,从而保证了原始数据的线程安全性。
底层原理
CopyOnWriteArrayList的底层原理是通过使用一个可变的数组来存储数据。在对数组进行修改(添加/删除)操作时,并不会直接修改原始数组,而是创建一个新的副本,并在副本上进行修改。这意味着对数组进行修改不会影响到正在进行迭代的线程,因此可以实现并发安全性。
具体实现步骤如下:
- 初始化时,CopyOnWriteArrayList会创建一个空数组,用于存储元素。
- 当添加元素时,CopyOnWriteArrayList会创建一个当前数组长度 + 1 大小的新数组,并将原始数组中的所有元素复制到新数组中,然后再将新元素添加到新数组的最后。
- 当删除元素时,CopyOnWriteArrayList会创建一个当前数组长度 - 1 大小的新数组,并将原始数组中除了要删除的元素之外的其他元素复制到新数组中。
通过这种方式,在对数组进行修改时,不会改变原始数组,而是创建一个新的副本,从而实现了并发安全性。同时,通过使用读写分离的思想,读操作可以并发进行,不需要加锁,提高了读操作的性能。
需要注意的是,由于每次修改操作都会创建一个新的数组副本,因此CopyOnWriteArrayList的内存消耗较高,适用于读操作较多而写操作较少的场景。
版权归原作者 Java张俊华 所有, 如有侵权,请联系我们删除。