为什么ArrayList 是非线程安全的集合类?
arduino
public class Demo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add("1");
System.out.println(list);
},String.valueOf(i)).start();
}
}
}


运行结果会报错,或者打印出来的不对、
原因
ArrayList 是非线程安全的集合类
多个线程同时对 list 进行修改操作(add)和读取操作(toString 内部会遍历)
当一个线程正在遍历时,另一个线程修改了集合结构,就会抛出 ConcurrentModificationException
解决方案
1.使用synchronized关键字
typescript
public class Demo {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Object lock = new Object();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
list.add("1");
System.out.println(list);
}
}, String.valueOf(i)).start();
}
}
}
2.使用vector类
Vector 是 Java 中的一个线程安全的集合类
与 ArrayList 相比,Vector 的方法都是同步的(synchronized)
适用于多线程环境下的使用场景

查看源码可以看到里边就是使用的synchronized关键字。
3.使用Collections.synchronizedList()工具类的方法
typescript
public class Demo {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
Object lock = new Object();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (lock) {
list.add("1");
System.out.println(list);
}
}, String.valueOf(i)).start();
}
}
4.使用 CopyOnWriteArrayList(推荐)
arduino
import java.util.concurrent.CopyOnWriteArrayList;
public class Demo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
list.add("1");
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
CopyOnWriteArrayList
CopyOnWriteArrayList这个是专门设计解决并发环境的线程安全集合。
我们去查看源码:
写操作

你看到这里边也使用了synchronized关键字,使用了lock;这是因为写的时候需要保证数据的一致性。
但是读操作就不需要加锁

CopyOnWriteArrayList 是如何工作的 ?
- 读操作 (如
get()、size())是非常高效的,因为它不涉及锁。每次读操作直接访问底层的数组(它的副本),从而保证了线程安全。 - 写操作 (如
add()、remove()、set())需要复制整个数组,修改副本,然后用新的数组替换旧的数组。这种策略避免了读操作被修改中的数组影响,但同时,写操作在多线程中必须进行同步,以防止在修改数组时出现不一致。
为什么同时使用 synchronized 和 lock?
在CopyOnWriteArrayList 中使用了synchronized与lock;
synchronized 与 lock 的区别:
synchronized是 Java 内置的同步机制 ,直接在方法或代码块上加锁,锁的粒度较大,通常适用于简单的同步需求。lock是通过显式的锁 (如ReentrantLock)来控制访问,它提供了比synchronized更强大的功能,如尝试加锁(tryLock()),中断锁等待等。lock允许更精细的控制,可以在获取锁时设置超时、获取公平性等。
在 CopyOnWriteArrayList 中的应用:
CopyOnWriteArrayList是线程安全的 集合类,它的底层实现通常会在添加、删除、更新元素时创建一个新的数组,并通过锁来确保写操作的线程安全。lock和synchronized可能用于不同场景中的加锁:- 使用
lock:
-
- 在
CopyOnWriteArrayList的内部,lock可能用于更精细的控制,特别是在 写操作 时对底层数组进行复制和修改的过程中。lock提供了 可中断的锁 、尝试获取锁 、公平性控制 等高级功能,这些都可能被用在性能要求较高的场景中。
- 在
- 使用
synchronized:
-
synchronized可能用于 其他部分的同步 ,例如在某些写操作(如add())的开始和结束之间。synchronized更适合对临界区(即操作共享资源的代码块)进行加锁。
ini
public boolean add(E e) {
synchronized (lock) { // 使用 lock 来同步
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
在这段源码中,synchronized (lock) 用于 同步访问,保证多个线程在执行添加元素的操作时不会发生数据冲突。
总结
优点 :线程安全、高效读操作、不需要显式加锁、适用于读多写少 的场景。
缺点 :写操作性能差、内存开销大、不适合频繁写操作 的场景。
适用场景:适用于读多写少、写操作较少、并发读操作频繁的场景。
CopyOnWriteArrayList 在某些高并发环境中是非常有用的,尤其是希望避免锁竞争、提高读取性能 时。然而,它并不适合所有的场景,特别是写操作较多的情况下,可能需要考虑其他更适合的并发数据结构,如 ConcurrentLinkedQueue 或 ReentrantLock 等。