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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor