Java 入门指南:Java 并发编程 —— Copy-On-Write 写时复制技术
文章目录
- Copy-On-Write
- 使用场景
- 特点
- 缺点
- CopyOnWrite 和 读写锁
- 相同点之处
- 不同之处
- CopyOnWriteArrayList
- 适用场景
- 主要特性
- 方法
- 构造方法
- CopyOnWriteArrayList 使用示例
- CopyOnWriteArraySet
- 适用场景
- 主要特性
- 方法
- 构造方法
- 使用注意事项
- CopyOnWriteArraySet 使用示例
Copy-On-Write
CopyOnWrite
是 Java 中一种常用的并发编程技术,指的是在修改共享资源时,不直接修改原始数据,而是在新的副本上进行操作,并最终将修改结果写回原始数据。它的核心思想是:可以容忍读操作并发,但写操作需要互斥执行(写时复制),牺牲了数据的实时性。这种技术通过减少数据共享时的并发冲突,提高了系统的整体效率和稳定性。
使用场景
-
并发集合:在 Java 中,
CopyOnWriteArrayList
和CopyOnWriteArraySet
就是基于Copy-on-Write模式实现的线程安全集合。这些集合适用于读多写少的并发场景,能够显著提高读操作的性能。 -
操作系统中的进程和内存管理:在UNIX类操作系统中,fork()系统调用创建子进程时,父进程和子进程会共享相同的内存页面,并将这些页面标记为写时复制。当任何一个进程尝试修改这些共享页面时,操作系统会创建页面的副本,并在副本上进行修改,从而保证了进程间的内存隔离和独立性。
-
数据库系统:在数据库系统中,Copy-on-Write 模式可以用于实现 MVCC(多版本并发控制)等机制,以支持事务的隔离性和一致性。
一个典型的使用场景是缓存更新。我们可以将缓存数据存储在一个副本中,读操作直接返回该副本的数据,而不影响缓存的读取。当需要更新缓存数据时,可以使用 CopyOnWrite
技术创建一个新的副本进行修改,同时保证读操作的连续性,而不会影响到线程安全。
由于每次写操作都需要创建全新的副本,因此在频繁进行写操作的场景下,使用 CopyOnWrite
技术可能会造成性能瓶颈。对于这种情况,可以考虑使用其他的线程安全集合实现。
特点
CopyOnWrite
技术的特点是写操作慢,但读操作快。因为每次写操作都需要创建一个全新的副本,在复制数据到副本的同时,读操作仍然可以并发访问原始数据。这种设计可以避免写和读操作并发执行而导致的数据不一致问题。
-
读写分离:
Copy-on-Write
模式实现了数据的读写分离,即读操作和写操作分别在不同的数据副本上进行,避免了并发访问时的冲突。 -
延迟复制:只有在数据需要被修改时,才会进行数据的复制操作,这是一种懒惰复制策略,有助于减少不必要的内存和CPU开销。
-
线程安全:在并发编程中,
Copy-on-Write
模式提供了一种高效的线程安全解决方案,允许多个线程同时读取数据而无需加锁。
缺点
-
内存占用问题:因为
CopyOnWrite
的写时复制机制,在进行写操作的时候,内存里会同时有两个对象,旧的对象和新写入的对象,分析 add 方法的时候大家都看到了。如果这些对象占用的内存比较大,比如 200M ,那么再写入 100M 数据进去,内存就会占用 600M,那么这时候就会造成频繁的
minor GC
和major GC
。 -
数据一致性问题:CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。对于希望写入的的数据马上能读到的场景,最好通过 ReentrantReadWriteLock 自定义一个列表。
CopyOnWrite 和 读写锁
相同点之处
-
线程安全:CopyOnWrite 和读写锁都提供了线程安全的数据结构或机制,使得多个线程可以安全地共享数据而不会导致数据不一致的问题。
-
支持并发读取:它们都允许多个线程同时读取数据而不进行加锁,从而提高了读取操作的性能。
-
读写分离:两者都区分了读操作和写操作,尽可能减少了读写冲突带来的性能损失。
不同之处
-
实现机制:
-
CopyOnWrite 采用的是写时复制的策略,即在执行写操作(如添加、删除等)时,会创建数据的一个新副本,并将修改应用到新副本上,然后再替换旧的数据引用。这种方法在读取操作时不加锁,但在写操作时会产生较大的开销。
-
读写锁(如
ReentrantReadWriteLock
)则是通过使用不同的锁来区分读操作和写操作。读操作可以并发执行,但写操作会独占锁,阻止其他读写操作,直到写操作完成。
-
-
性能特点:
-
CopyOnWrite 在读多写少的场景下表现较好,因为读取操作不会被阻塞,但写操作由于需要复制整个数据结构,可能会消耗较多的内存和CPU资源。
-
读写锁 在写操作较少的情况下也能保持较高的性能,因为它只在写操作时才会阻塞其他操作。读操作可以并发执行,不会造成太大的性能损失。
-
-
内存消耗:
-
CopyOnWrite 在执行写操作时会创建数据的副本,因此在高并发写操作的场景下可能会导致较高的内存消耗。
-
读写锁 则不会产生额外的内存开销,因为它只是控制对现有数据的访问权限。
-
-
适用场景:
-
CopyOnWrite 更适合读多写少的场景,尤其是在写操作频率较低的情况下。
-
读写锁 适用于读写操作都较为频繁的场景,尤其是当写操作也较为常见时。
-
-
迭代器行为:
-
CopyOnWrite 的迭代器在迭代过程中是安全的,即使有其他线程在修改数据也不会抛出
ConcurrentModificationException
。 -
读写锁 的迭代器在迭代过程中如果数据被修改,则可能会抛出
ConcurrentModificationException
,除非使用了显式的锁来保护迭代过程。
-
-
并发级别:
-
CopyOnWrite 在读取操作时允许多个线程并发访问,但在写操作时需要复制整个数据结构,因此写操作是独占的。
-
读写锁 在读取操作时允许多个线程并发访问,而在写操作时也是独占的,但可以通过锁降级等方式优化性能。
-
CopyOnWriteArrayList
CopyOnWriteList
是 Java 中的一个线程安全的列表实现类,继承自 AbstractList
类,属于并发集合的一种。在需要并发读取列表数据的同时,保证写操作的可靠性和一致性。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
CopyOnWriteArrayList
内部维护的了一个数组,并使用 volatile
修饰,保证数据的可见性。在修改数组时,不是直接修改原数组,而是先复制一份原数组的副本,然后在副本上进行修改,最后将原数组的引用指向新的副本。这种机制保证了读操作的无锁性和高效性,非常适合读多写少的并发场景。
适用场景
CopyOnWriteArrayList
特别适用于读多写少的并发场景,例如:
-
在线新闻发布系统:新闻列表需要被频繁地读取(用户浏览新闻),但只偶尔被修改(发布新新闻或更新现有新闻)。
-
缓存数据:当缓存数据被多个线程频繁读取,但更新频率较低时,可以使用
CopyOnWriteArrayList
来存储缓存数据。
主要特性
CopyOnWriteList
的特点是它在对集合进行修改时(添加、删除、修改元素),不直接在原有集合上进行操作,而是创建一个新的副本进行修改。这种设计使得读操作可以在没有锁的情况下并发进行,从而提高了读操作的性能。
由于每次修改都会创建一个新的副本,因此 CopyOnWriteList
的修改操作会更慢,需要更多的内存开销。它更适用于读多写少的场景,比如数据一旦初始化后就很少修改的情况。
CopyOnWriteList
实现了List 接口,因此可以像普通的列表一样使用它,例如添加元素、删除元素、获取元素等操作。
由于 CopyOnWriteList
的修改操作是基于副本进行的,因此对其进行修改的操作,在不同的线程中可能看不到立即的更新。
方法
由于 CopyOnWriteArrayList
使用 CopyOnWrite
技术,在修改列表时会创建一个新的副本。因此,修改操作(例如 add
、remove
、set
等)会比较慢,并且消耗较多的内存。但是,读操作(例如 get
、contains
等)是高效的,不需要锁定。
由于 CopyOnWriteArrayList
继承自 AbstractList
类,所以它也具有 AbstractList
类中定义的一些方法,例如 add(int index, E element)
、remove(int index)
、iterator()
等
构造方法
- 创建一个初始为空的
CopyOnWriteArrayList
。
CopyOnWriteArrayList<>();
- 创建一个包含指定集合中的元素的
CopyOnWriteArrayList
。
CopyOnWriteArrayList(Collection<? extends E> collection)
- 创建一个包含指定数组中的元素的
CopyOnWriteArrayList
。
CopyOnWriteArrayList(E[] toCopyIn)
CopyOnWriteArrayList 使用示例
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("Apple");
list.add("Banana");
list.add("Cherry");
// 打印列表
System.out.println("原始列表: " + list);
// 创建线程修改列表
Thread modifyThread = new Thread(() -> {
list.remove("Banana");
list.add("Durian");
System.out.println("修改后的列表: " + list);
});
// 创建线程读取列表
Thread readThread = new Thread(() -> {
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println("读取到的元素: " + iterator.next());
}
});
// 启动线程
modifyThread.start();
readThread.start();
try {
// 等待线程结束
modifyThread.join();
readThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主线程被中断");
}
}
}
CopyOnWriteArraySet
CopyOnWriteSet
是 Java 中的一个线程安全的集合实现类,实现了 Set
接口,属于并发集合的一种。
CopyOnWriteSet
是基于 CopyOnWriteArrayList
实现的,它使用一个内部的 CopyOnWriteArrayList
来存储元素。而 CopyOnWriteSet
具备了 Set
的特性,其中的元素是唯一的且无序的。
CopyOnWriteSet
的特点与 CopyOnWriteList
类似,它在对集合进行修改时(添加、删除元素),不直接在原有集合上进行操作,而是创建一个新的副本进行修改。这种设计使得读操作可以在没有锁的情况下并发进行,从而提高了读操作的性能。更适用于读多写少的场景
由于 CopyOnWriteSet
实现了 Set
接口,因此可以像普通的集合一样使用它。由于它的修改操作是基于副本进行的,因此对 CopyOnWriteSet
进行修改的操作,在不同的线程中也可能看不到立即的更新。
适用场景
CopyOnWriteArraySet
也特别适用于读多写少的并发场景,如缓存、配置信息的存储等。在这些场景中,数据的读取操作远多于写入操作,因此可以充分利用 CopyOnWriteArraySet
的读操作高效性,同时避免写操作时的线程安全问题。
主要特性
-
线程安全:
CopyOnWriteArraySet
通过内部的CopyOnWriteArrayList
保证了集合的线程安全性,允许多个线程同时读取集合内容,而无需进行外部同步。 -
无序性:
CopyOnWriteArraySet
是一个无序集合,元素的存储顺序是不确定的。 -
写时复制:在修改集合(如添加或删除元素)时,会先复制当前集合的一个副本,然后在副本上进行修改,最后将原集合的引用指向新的副本。这种机制避免了写操作时的线程冲突,但增加了写操作的开销。
-
读操作高效:由于读操作直接访问原集合,且无需加锁,因此读操作的速度非常快。
-
写操作开销大:每次写操作都需要复制整个集合,如果集合中的数据量较大,写操作可能会比较耗时,并占用较多的内存。
方法
构造方法
- 创建一个初始为空的
CopyOnWriteArraySet
。
CopyOnWriteArraySet<>();
- 创建一个包含指定集合中的元素的
CopyOnWriteArraySet
。
CopyOnWriteArraySet(Collection<? extends E> collection)
使用注意事项
-
内存占用:由于写操作会复制整个集合,因此在数据量较大时,
CopyOnWriteArraySet
可能会占用较多的内存。 -
数据一致性:
CopyOnWriteArraySet
只能保证数据的最终一致性,即在写操作完成后的一段时间内(通常是下一次读操作前),新写入的数据才能被读取到。如果需要实时读取最新数据,则不适合使用CopyOnWriteArraySet
。 -
不支持null元素:与 HashSet 不同,
CopyOnWriteArraySet
不允许存储null元素。如果尝试添加null元素,将抛出NullPointerException异常。
CopyOnWriteArraySet 使用示例
CopyOnWriteArraySet
是一个基于 CopyOnWriteArrayList
的线程安全的集合,它保证了元素的唯一性。它同样采用了写时复制的策略来保证读操作的安全性。
import java.util.concurrent.CopyOnWriteArraySet;
public class CopyOnWriteArraySetExample {
public static void main(String[] args) {
// 创建 CopyOnWriteArraySet
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
// 添加元素
set.add("Apple");
set.add("Banana");
set.add("Cherry");
set.add("Banana"); // 尝试添加重复元素
// 打印集合
System.out.println("原始集合: " + set);
// 创建线程修改集合
Thread modifyThread = new Thread(() -> {
set.remove("Banana");
set.add("Durian");
System.out.println("修改后的集合: " + set);
});
// 创建线程读取集合
Thread readThread = new Thread(() -> {
for (String element : set) {
System.out.println("读取到的元素: " + element);
}
});
// 启动线程
modifyThread.start();
readThread.start();
try {
// 等待线程结束
modifyThread.join();
readThread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("主线程被中断");
}
}
}