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

server/2024/10/18 10:14:40/

文章目录

    • Copy-On-Write
      • 使用场景
      • 特点
      • 缺点
      • CopyOnWrite 和 读写锁
        • 相同点之处
        • 不同之处
    • CopyOnWriteArrayList
      • 适用场景
      • 主要特性
      • 方法
      • 构造方法
      • CopyOnWriteArrayList 使用示例
    • CopyOnWriteArraySet
      • 适用场景
      • 主要特性
      • 方法
      • 构造方法
      • 使用注意事项
      • CopyOnWriteArraySet 使用示例

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 类,属于并发集合的一种。在需要并发读取列表数据的同时,保证写操作的可靠性和一致性。

java">/** 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
java">CopyOnWriteArrayList<>();
  1. 创建一个包含指定集合中的元素的 CopyOnWriteArrayList
java">CopyOnWriteArrayList(Collection<? extends E> collection)
  1. 创建一个包含指定数组中的元素的 CopyOnWriteArrayList
java">CopyOnWriteArrayList(E[] toCopyIn)

CopyOnWriteArrayList 使用示例

CopyOnWriteArrayList 是一个线程安全的列表实现,它在执行写入操作(如添加、删除等)时会创建整个列表的一个新副本,并将修改应用到新副本上,然后替换旧的列表引用。这样可以保证读取操作不会受到写入操作的影响,从而简化了并发访问的同步问题。

java">import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;public class CopyOnWriteArrayListExample {public static void main(String[] args) {// 创建 CopyOnWriteArrayListCopyOnWriteArrayList<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
java">CopyOnWriteArraySet<>();
  1. 创建一个包含指定集合中的元素的 CopyOnWriteArraySet
java">CopyOnWriteArraySet(Collection<? extends E> collection)

使用注意事项

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

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

  3. 不支持null元素:与 HashSet 不同,CopyOnWriteArraySet 不允许存储null元素。如果尝试添加null元素,将抛出NullPointerException异常。

CopyOnWriteArraySet 使用示例

CopyOnWriteArraySet 是一个基于 CopyOnWriteArrayList 的线程安全的集合,它保证了元素的唯一性。它同样采用了写时复制的策略来保证读操作的安全性。

java">import java.util.concurrent.CopyOnWriteArraySet;public class CopyOnWriteArraySetExample {public static void main(String[] args) {// 创建 CopyOnWriteArraySetCopyOnWriteArraySet<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("主线程被中断");}}
}

http://www.ppmy.cn/server/114831.html

相关文章

解决服务器VS Code中Jupyter突然崩溃的问题

问题 本来在服务器Anaconda的Python环境里装其他的包&#xff0c;装完了想在Jupyter里写代码验证一下有没有装好&#xff0c;一运行发现Jupyter崩溃了&#xff01;&#xff1f;报错如下所示 Failed to start the Kernel. ImportError: /home/hujh/anaconda3/envs/mia/lib/pyt…

认识GO语言中的nil,零值与空结构体

go语言的初学者&#xff0c;特别是java开发者新学习go语言&#xff0c;对于一些和java类似但是又有差异的概念很容易混淆&#xff0c;比如说go中的零值&#xff0c;nil 和 空结构体。本文就来详细探讨一下go中这些特殊概念的含义和实际场景中的应用&#xff1a; 零值 零值&…

Linux 技巧汇编

10个重要的Linux ps命令实战 显示所有当前进程 根据用户过滤进程 通过cpu和内存使用来过滤进程 通过进程名和PID过滤 根据线程来过滤进程 树形显示进程 显示安全信息 格式化输出root用户&#xff08;真实的或有效的UID&#xff09;创建的进程 使用PS实时监控进程状态 …

Spring和SpringBoot的关系是什么?

Spring Boot可以帮助我们快速开发Spring程序 Spring和SpringBoot是两个相互关联但又有所区别的Java开发框架&#xff0c;它们之间的关系主要体现在以下几个方面&#xff1a; 一、基础概念 Spring&#xff1a;Spring是一个广泛应用的开源Java框架&#xff0c;它提供了一系列模…

FLY GCS:无人机领域的核心指挥与控制中枢!!!

其主要功能包括但不限于以下几个方面 飞行任务规划 用户可以在FLY GCS软件中预先规划整个飞行航线&#xff0c;包括起飞点、航点、降落点等&#xff0c;以及预设拍照、录像、空投等作业动作。这种规划功能使得无人机能够按照预定的路线和指令执行任务&#xff0c;大大提高了任…

音乐网站-前后台登录注册搜索试听下载评论音乐分计算机毕业设计/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序

1. 前台功能模块 首页&#xff1a; 展示热门音乐、推荐音乐、最新发布。搜索框&#xff1a;支持音乐、专辑、艺人等的搜索。用户登录/注册入口。 用户注册和登录&#xff1a; 用户注册&#xff1a;输入用户名、密码、邮箱等信息。用户登录&#xff1a;输入用户名和密码。密码找…

个人随想-如何开发一个code agent

随着sonnet的普及&#xff0c;现在的开发确实可以达到事半功倍的效果&#xff0c;再加上cursor、claude dev等工具的加持&#xff0c;现在的软件开发&#xff0c;确实门槛降低了很多&#xff0c;我们可以快速的让ai给我们大量的提示、重构、单元测试、explain甚至是完全用自然语…

今日(2024 年 9 月 10 日)科技新闻

芯海科技取得触控装置及电子设备专利&#xff1a;天眼查知识产权信息显示&#xff0c;芯海科技&#xff08;深圳&#xff09;股份有限公司取得 “一种触控装置及电子设备” 专利&#xff0c;授权公告号 cn221686929u&#xff0c;申请日期为 2023 年 9 月。此专利的触控装置包括…