一、CopyOnWriteArrayLists 是什么
CopyOnWriteArrayList
是 Java 中并发编程中的一个工具类,它是 ArrayList
的线程安全版本。
1、 与普通的 ArrayList 不同,
CopyOnWriteArrayList
在进行修改操作时并不直接操作原始数据,而是先复制一份原始数据,在副本上进行修改,最后再用修改后的副本替换原始数据。这样,读操作可以直接访问原始数据而不需要加锁,提高了读操作的性能,但写操作的性能相对较低。
2、原理
CopyOnWriteArrayList 的原理是在写操作时,创建一个新的数组,将新数据写入新数组,然后用新数组替换原始数组。这样可以确保在读操作中不会受到写操作的影响,实现了读写分离。
具体步骤如下:
-
读操作: 读操作直接在当前的数组上进行,不需要加锁,因为读操作不会对数据进行修改,多个线程可以并发进行读操作。
-
写操作:
写操作在修改数据之前,先复制一份当前的数组,然后在新数组上进行修改。写操作完成后,用新数组替换原始数组。这样,写操作不会影响到正在进行的读操作。
-
删除
remove 方法也采用了类似的方式使用 ReentrantLock 来进行加锁,确保在对数组进行修改的过程中不会被其他线程干扰。-
这个 remove 方法用于删除指定索引位置上的元素。在获取锁之后,它首先获取当前数组,然后根据给定的索引位置获取要删除的元素,并通过数组拷贝的方式生成新的数组,将目标元素排除在外。最后通过 setArray 方法将新的数组设置回去。
-
同样,使用 finally 块确保在任何情况下都能够释放锁。
-
这种通过加锁来保护写操作的方式是典型的写时复制(Copy-On-Write)策略,它使得读操作可以在不加锁的情况下进行,提高了读操作的性能,但在写操作时需要进行数组的复制,因此写操作的开销较大。这种适用于读多写少的场景。
由于写操作是在新数组上进行的,所以写操作不会阻塞正在进行的读操作。这使得 CopyOnWriteArrayList
在读多写少的场景中表现良好。
需要注意的是,由于写操作是在新数组上进行,所以写操作的代价相对较高,尤其是在数组较大的情况下。因此,CopyOnWriteArrayList
适用于读多写少的场景,而不适合写操作频繁的高并发场景。
3、设计思路:
- 写时复制: 主要特点是写操作时创建一个新的数组,将修改后的数据写入新数组,然后用新数组替换原数组。这样做的好处是在写操作时不会影响正在进行的读操作,实现了读写分离。
- 加锁保证原子性: 在写操作时使用
synchronized
关键字对关键代码块进行加锁,确保在写操作过程中其他线程不会修改数组。这样可以保证写操作的原子性。上图写操作的源码中使用了
ReentrantLock
来进行加锁。这是一种替代synchronized
关键字的方式,它提供了更灵活的锁定和解锁机制,并支持可重入性。 - 数组拷贝: 写操作在修改数据之前,会先将当前数组进行拷贝,然后在新数组上进行修改。这样可以避免写操作影响正在进行的读操作。
- 适用于读多写少场景: 由于写操作涉及数组拷贝,写操作的代价相对较高,适用于读多写少的场景。
4、可以应用到其他设计:
- 缓存更新: 在需要频繁读取但很少更新的缓存场景中,可以使用类似的读写分离的设计思路,通过写时复制的方式保证在更新缓存时不影响正在进行的读取操作。
- 配置管理: 在配置管理中,如果有多个线程读取配置信息而很少修改配置信息的情况下,可以使用类似的读写分离机制,确保在修改配置信息时不影响正在读取配置信息的线程。
二、为什么要用 CopyOnWriteArrayLists
线程安全: CopyOnWriteArrayList
提供了一种线程安全的读写操作机制。在读操作中,不需要加锁,而写操作则通过复制原始数据,避免了对原始数据的并发修改。
避免读写冲突: 在传统的 ArrayList 中,如果一个线程在读取的同时,另一个线程正在修改列表,可能导致读写冲突,需要额外的同步机制。而 CopyOnWriteArrayList
通过写时复制的方式,确保在写操作时不会影响正在进行的读操作,避免了读写冲突。
适用于读多写少的场景: 由于写操作需要复制整个数组,因此写操作的性能较差。但在读多写少的场景中,由于读操作不需要加锁,可以提高读操作的性能。
支持弱一致性迭代器: CopyOnWriteArrayList
的迭代器可以提供弱一致性,即在迭代的过程中如果有其他线程对列表进行修改,迭代器仍然可以继续遍历旧的数据。这在某些场景下是合理的。
应用于特定场景: 适用于一些特定的场景,例如配置管理、事件监听器列表等,其中读操作较为频繁,写操作较为稀少。或者设计框架的时候,用来配置拦截器等数据。
需要注意的是,CopyOnWriteArrayList
的适用性并非一刀切,它在某些场景下有优势,但在某些高并发写操作频繁的场景下,可能不是最佳选择。开发人员需要根据具体场景和需求进行选择。
三、如何应用 CopyOnWriteArrayLists:
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListExample {
public static void main(String[] args) {
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// 写操作
list.add("Item1");
list.add("Item2");
list.add("Item3");
// 读操作
for (String item : list) {
System.out.println(item);
}
}
}
在上面的例子中,CopyOnWriteArrayList
被用于存储字符串,并且在迭代时不需要额外的同步措施。