Java 入门指南:Java 并发编程 —— Copy-On-Write 写时复制技术

Copy-On-Write

CopyOnWrite 是 Java 中一种常用的并发编程技术,指的是在修改共享资源时,不直接修改原始数据,而是在新的副本上进行操作,并最终将修改结果写回原始数据。它的核心思想是:可以容忍读操作并发,但写操作需要互斥执行(写时复制),牺牲了数据的实时性。这种技术通过减少数据共享时的并发冲突,提高了系统的整体效率和稳定性。

使用场景

  1. 并发集合:在 Java 中,CopyOnWriteArrayListCopyOnWriteArraySet 就是基于Copy-on-Write模式实现的线程安全集合。这些集合适用于读多写少的并发场景,能够显著提高读操作的性能。

  2. 操作系统中的进程和内存管理:在UNIX类操作系统中,fork()系统调用创建子进程时,父进程和子进程会共享相同的内存页面,并将这些页面标记为写时复制。当任何一个进程尝试修改这些共享页面时,操作系统会创建页面的副本,并在副本上进行修改,从而保证了进程间的内存隔离和独立性。

  3. 数据库系统:在数据库系统中,Copy-on-Write 模式可以用于实现 MVCC(多版本并发控制)等机制,以支持事务的隔离性和一致性。

一个典型的使用场景是缓存更新。我们可以将缓存数据存储在一个副本中,读操作直接返回该副本的数据,而不影响缓存的读取。当需要更新缓存数据时,可以使用 CopyOnWrite 技术创建一个新的副本进行修改,同时保证读操作的连续性,而不会影响到线程安全。

由于每次写操作都需要创建全新的副本,因此在频繁进行写操作的场景下,使用 CopyOnWrite 技术可能会造成性能瓶颈。对于这种情况,可以考虑使用其他的线程安全集合实现。

特点

CopyOnWrite 技术的特点是写操作慢,但读操作快。因为每次写操作都需要创建一个全新的副本,在复制数据到副本的同时,读操作仍然可以并发访问原始数据。这种设计可以避免写和读操作并发执行而导致的数据不一致问题。

  1. 读写分离Copy-on-Write 模式实现了数据的读写分离,即读操作和写操作分别在不同的数据副本上进行,避免了并发访问时的冲突。

  2. 延迟复制:只有在数据需要被修改时,才会进行数据的复制操作,这是一种懒惰复制策略,有助于减少不必要的内存和CPU开销。

  3. 线程安全:在并发编程中,Copy-on-Write 模式提供了一种高效的线程安全解决方案,允许多个线程同时读取数据而无需加锁。

缺点

  1. 内存占用问题:因为 CopyOnWrite 的写时复制机制,在进行写操作的时候,内存里会同时有两个对象,旧的对象和新写入的对象,分析 add 方法的时候大家都看到了。

    如果这些对象占用的内存比较大,比如 200M ,那么再写入 100M 数据进去,内存就会占用 600M,那么这时候就会造成频繁的 minor GCmajor GC

  2. 数据一致性问题:CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。对于希望写入的的数据马上能读到的场景,最好通过 ReentrantReadWriteLock 自定义一个列表。

CopyOnWrite 和 读写锁

相同点之处
  1. 线程安全CopyOnWrite 和读写锁都提供了线程安全的数据结构或机制,使得多个线程可以安全地共享数据而不会导致数据不一致的问题。

  2. 支持并发读取:它们都允许多个线程同时读取数据而不进行加锁,从而提高了读取操作的性能。

  3. 读写分离:两者都区分了读操作和写操作,尽可能减少了读写冲突带来的性能损失。

不同之处
  1. 实现机制

    • CopyOnWrite 采用的是写时复制的策略,即在执行写操作(如添加、删除等)时,会创建数据的一个新副本,并将修改应用到新副本上,然后再替换旧的数据引用。这种方法在读取操作时不加锁,但在写操作时会产生较大的开销。

    • 读写锁(如 ReentrantReadWriteLock)则是通过使用不同的锁来区分读操作和写操作。读操作可以并发执行,但写操作会独占锁,阻止其他读写操作,直到写操作完成。

  2. 性能特点

    • CopyOnWrite 在读多写少的场景下表现较好,因为读取操作不会被阻塞,但写操作由于需要复制整个数据结构,可能会消耗较多的内存和CPU资源。

    • 读写锁 在写操作较少的情况下也能保持较高的性能,因为它只在写操作时才会阻塞其他操作。读操作可以并发执行,不会造成太大的性能损失。

  3. 内存消耗

    • CopyOnWrite 在执行写操作时会创建数据的副本,因此在高并发写操作的场景下可能会导致较高的内存消耗。

    • 读写锁 则不会产生额外的内存开销,因为它只是控制对现有数据的访问权限。

  4. 适用场景

    • CopyOnWrite 更适合读多写少的场景,尤其是在写操作频率较低的情况下。

    • 读写锁 适用于读写操作都较为频繁的场景,尤其是当写操作也较为常见时。

  5. 迭代器行为

    • CopyOnWrite 的迭代器在迭代过程中是安全的,即使有其他线程在修改数据也不会抛出 ConcurrentModificationException

    • 读写锁 的迭代器在迭代过程中如果数据被修改,则可能会抛出 ConcurrentModificationException,除非使用了显式的锁来保护迭代过程。

  6. 并发级别

    • 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 的修改操作是基于副本进行的,因此对其进行修改的操作,在不同的线程中可能看不到立即的更新。

方法

![[Collection 接口#List 接口常用方法|List 接口 方法]]

由于 CopyOnWriteArrayList 使用 CopyOnWrite 技术,在修改列表时会创建一个新的副本。因此,修改操作(例如 addremoveset 等)会比较慢,并且消耗较多的内存。但是,读操作(例如 getcontains 等)是高效的,不需要锁定。

由于 CopyOnWriteArrayList 继承自 AbstractList 类,所以它也具有 AbstractList 类中定义的一些方法,例如 add(int index, E element)remove(int index)iterator()

构造方法

  1. 创建一个初始为空的 CopyOnWriteArrayList
CopyOnWriteArrayList<>();
  1. 创建一个包含指定集合中的元素的 CopyOnWriteArrayList
CopyOnWriteArrayList(Collection<? extends E> collection)
  1. 创建一个包含指定数组中的元素的 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 的读操作高效性,同时避免写操作时的线程安全问题。

主要特性

  1. 线程安全CopyOnWriteArraySet 通过内部的 CopyOnWriteArrayList 保证了集合的线程安全性,允许多个线程同时读取集合内容,而无需进行外部同步。

  2. 无序性CopyOnWriteArraySet 是一个无序集合,元素的存储顺序是不确定的。

  3. 写时复制:在修改集合(如添加或删除元素)时,会先复制当前集合的一个副本,然后在副本上进行修改,最后将原集合的引用指向新的副本。这种机制避免了写操作时的线程冲突,但增加了写操作的开销。

  4. 读操作高效:由于读操作直接访问原集合,且无需加锁,因此读操作的速度非常快。

  5. 写操作开销大:每次写操作都需要复制整个集合,如果集合中的数据量较大,写操作可能会比较耗时,并占用较多的内存。

方法

![[Collection 接口#Set 接口常用方法]]

构造方法

  1. 创建一个初始为空的 CopyOnWriteArraySet
CopyOnWriteArraySet<>();
  1. 创建一个包含指定集合中的元素的 CopyOnWriteArraySet
CopyOnWriteArraySet(Collection<? extends E> collection)

使用注意事项

  1. 内存占用:由于写操作会复制整个集合,因此在数据量较大时,CopyOnWriteArraySet 可能会占用较多的内存。

  2. 数据一致性CopyOnWriteArraySet 只能保证数据的最终一致性,即在写操作完成后的一段时间内(通常是下一次读操作前),新写入的数据才能被读取到。如果需要实时读取最新数据,则不适合使用 CopyOnWriteArraySet

  3. 不支持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("主线程被中断");
        }
    }
}
  • 29
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值