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

2024-09-07 07:04

本文主要是介绍Java 入门指南:Java 并发编程 —— Copy-On-Write 写时复制技术,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

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

/** 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) {// 创建 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
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) {// 创建 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("主线程被中断");}}
}

这篇关于Java 入门指南:Java 并发编程 —— Copy-On-Write 写时复制技术的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1144386

相关文章

SpringBoot使用Apache Tika检测敏感信息

《SpringBoot使用ApacheTika检测敏感信息》ApacheTika是一个功能强大的内容分析工具,它能够从多种文件格式中提取文本、元数据以及其他结构化信息,下面我们来看看如何使用Ap... 目录Tika 主要特性1. 多格式支持2. 自动文件类型检测3. 文本和元数据提取4. 支持 OCR(光学

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

Java 字符数组转字符串的常用方法

《Java字符数组转字符串的常用方法》文章总结了在Java中将字符数组转换为字符串的几种常用方法,包括使用String构造函数、String.valueOf()方法、StringBuilder以及A... 目录1. 使用String构造函数1.1 基本转换方法1.2 注意事项2. 使用String.valu

java脚本使用不同版本jdk的说明介绍

《java脚本使用不同版本jdk的说明介绍》本文介绍了在Java中执行JavaScript脚本的几种方式,包括使用ScriptEngine、Nashorn和GraalVM,ScriptEngine适用... 目录Java脚本使用不同版本jdk的说明1.使用ScriptEngine执行javascript2.

Spring MVC如何设置响应

《SpringMVC如何设置响应》本文介绍了如何在Spring框架中设置响应,并通过不同的注解返回静态页面、HTML片段和JSON数据,此外,还讲解了如何设置响应的状态码和Header... 目录1. 返回静态页面1.1 Spring 默认扫描路径1.2 @RestController2. 返回 html2

Python使用qrcode库实现生成二维码的操作指南

《Python使用qrcode库实现生成二维码的操作指南》二维码是一种广泛使用的二维条码,因其高效的数据存储能力和易于扫描的特点,广泛应用于支付、身份验证、营销推广等领域,Pythonqrcode库是... 目录一、安装 python qrcode 库二、基本使用方法1. 生成简单二维码2. 生成带 Log

Spring常见错误之Web嵌套对象校验失效解决办法

《Spring常见错误之Web嵌套对象校验失效解决办法》:本文主要介绍Spring常见错误之Web嵌套对象校验失效解决的相关资料,通过在Phone对象上添加@Valid注解,问题得以解决,需要的朋... 目录问题复现案例解析问题修正总结  问题复现当开发一个学籍管理系统时,我们会提供了一个 API 接口去

Java操作ElasticSearch的实例详解

《Java操作ElasticSearch的实例详解》Elasticsearch是一个分布式的搜索和分析引擎,广泛用于全文搜索、日志分析等场景,本文将介绍如何在Java应用中使用Elastics... 目录简介环境准备1. 安装 Elasticsearch2. 添加依赖连接 Elasticsearch1. 创

Spring核心思想之浅谈IoC容器与依赖倒置(DI)

《Spring核心思想之浅谈IoC容器与依赖倒置(DI)》文章介绍了Spring的IoC和DI机制,以及MyBatis的动态代理,通过注解和反射,Spring能够自动管理对象的创建和依赖注入,而MyB... 目录一、控制反转 IoC二、依赖倒置 DI1. 详细概念2. Spring 中 DI 的实现原理三、