List接口实现类(3):CopyOnWriteArrayList

Java专栏: 线程并发安全中你须掌握的CopyOnWriteList Part1CopyOnWriteList简介 ArrayList是线程不安全的,于是JDK新增加了一个线程并发安全的List——CopyOnWriteList,中心思想就是copy-on-write,简单来说是读写分离:读时共享、写时复制(原本的array)更新(且为独占式的加锁),而我们下面分析的源码具体实现也是这个思想的体现。 继承体系: 我们单独看一下CopyOnWriteList的主要属性和下面要主要分析的方法有哪些。从图中看出: 每个CopyOnWriteList对象里面有一个ar 阅读详情

一、概述

1. 什么是CopyOnWrite

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,当想要对一块内存进行修改时,不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉。
  这是一种用于程序设计中的优化策略,是一种延时懒惰策略。

2. CopyOnWrite容器

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
  从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

3. 为什么使用CopyOnWrite容器

以ArrayList为例, ArrayList 并不是线程安全的,在读线程在读取 ArrayList 的时候如果有写线程在写数据的时候,基于 fast-fail 机制,会抛出ConcurrentModificationException异常,也就是说 ArrayList 并不是一个线程安全的容器,当然可以用 Vector,或者使用 Collections 的静态方法将 ArrayList 包装成一个线程安全的类,但是这些方式都是采用 java 关键字 synchronzied 对方法进行修饰,利用独占式锁来保证线程安全的。但是,由于独占式锁在同一时刻只有一个线程能够获取到对象监视器,很显然这种方式效率并不是太高。
  回到业务场景中,有很多业务往往是读多写少的,比如系统配置的信息,除了在初始进行系统配置的时候需要写入数据,其他大部分时刻其他模块之后对系统信息只需要进行读取,又比如白名单,黑名单等配置,只需要读取名单配置然后检测当前用户是否在该配置范围以内。类似的还有很多业务场景,它们都是属于读多写少的场景。如果在这种情况用到上述的方法,使用 Vector,Collections 转换的这些方式是不合理的,因为尽管多个读线程从同一个数据容器中读取数据,但是读线程对数据容器的数据并不会发生发生修改。通过读写分离的思想,使得读读之间不会阻塞,无疑如果一个 list 能够做到被多个读线程读取的话,性能会大大提升不少。但是,如果仅仅是将 list 通过读写锁(ReentrantReadWriteLock)进行再一次封装的话,由于读写锁的特性,当写锁被写线程获取后,读写线程都会被阻塞。如果仅仅使用读写锁对 list 进行封装的话,这里仍然存在读线程在读数据的时候被阻塞的情况,如果想 list 的读效率更高的话,这里就是我们的突破口,如果我们保证读线程无论什么时候都不被阻塞,效率岂不是会更高?
  Doug Lea 大师就为我们提供 CopyOnWriteArrayList 容器可以保证线程安全,保证读读之间在任何时候都不会被阻塞,CopyOnWriteArrayList 也被广泛应用于很多业务场景之中,CopyOnWriteArrayList 值得被我们好好认识一番。

4.设计思想

回到上面所说的,如果简单的使用读写锁的话,在写锁被获取之后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在读线程的角度来看,即读线程任何时候都是获取到最新的数据,满足数据实时性。既然我们说到要进行优化,必然有 trade-off,我们就可以牺牲数据实时性满足数据的最终一致性即可。而 CopyOnWriteArrayList 就是通过 Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。
  COW 通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对 CopyOnWrite 容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以 CopyOnWrite 容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。

二、源码解析

下面以为ArrayList 展示fail-fast机制导致的ConcurrentModificationException。另外详细对 CopyOnWriteArrayList 的源码进行解读

1. 问题案例

demo1:
获取list的迭代器,再向list里put值,循环获取迭代器里的值的时候就会发生 ConcurrentModificationException

/**
 * @author nowuseeme
 */
public class ArrayListConcurrentModifyDemo {
    
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        Iterator<Integer> iterator = list.iterator();
        list.add(4);
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }
}

原因:由于fail-fast机制,ArrayList的 iterator.hasNext()方法每次都会执行checkForComodification()方法检测当前ArrayList里面的 modCount与 迭代器获取时候的modCount是否一致,由于重新put值,导致modCount++,不一致抛出异常
demo2:
多线程操作模拟

/**
 * @author nowuseeme
 */
public class ArrayListConcurrentModifyDemo2 {

    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException{

        list.add(1);
        list.add(2);
        list.add(3);
        // 存放8个线程的线程池
        ExecutorService readExecutor = Executors.newFixedThreadPool(4);
        ExecutorService putExecutor = Executors.newFixedThreadPool(4);

        // 执行4个read任务(当前正在迭代集合(这里模拟并发中读取某一list的场景))
        for (int i = 0; i < 4; i++) {
            readExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    Iterator iterator = list.iterator();
                    while (iterator.hasNext()) {
                        try {
                            //模拟睡眠
                            Thread.sleep(1000L);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("iterator next value: "+iterator.next());

                    }
                }
            });
        }

        // 执行4个put任务
        for (int i = 0; i < 4; i++) {
            putExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    // 添加数据
                    list.add(4);
                    System.out.println("list add value 4");
                }
            });
        }
        System.out.println("list: "+ Arrays.toString(list.toArray()));
    }
}

2. CopyOnWriteArrayList源码解读

属性与get()方法

private transient volatile Object[] array;

final Object[] getArray() {
    return array;
}
final void setArray(Object[] a) {
    array = a;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

get 方法实现非常简单,几乎就是一个“单线程”程序,没有对多线程添加任何的线程安全控制,也没有加锁也没有 CAS 操作等等,原因是,所有的读线程只是会读取数据容器中的数据,并不会进行修改。

add()方法源码

 public boolean add(E e) {
    //1. 重入锁,保证写线程在同一时刻只有一个
    //   加锁防止多线程生成多个副本数组
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //2. 获取旧数组引用
        Object[] elements = getArray();
        int len = elements.length;
        //3. 创建新的数组,并将旧数组的数据复制到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //4. 往新数组中添加新的数据
        newElements[len] = e;
        //5. 将旧数组引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        //解锁
        lock.unlock();
    }
}

add 方法的逻辑也比较容易理解:

  1. 采用 ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据;
  2. 数组引用是 volatile 修饰的,因此将旧的数组引用指向新的数组,根据 volatile 的 happens-before 规则,写线程对数组引用的修改对读线程是可见的。
  3. 由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。

CopyOnWriteArrayList的迭代器

public Iterator<E> iterator() {
    return new COWIterator:<E>(getArray(), 0);
}

内部类COWIterator:

static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;
    
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    
    public boolean hasNext() {
        return cursor < snapshot.length;
    }
    
    public boolean hasPrevious() {
        return cursor > 0;
    }
    
    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
}

很明显,在迭代器内部,先CopyOnWriteArrayList内部的数组引用当成一个快照赋值给COWIterator的属性 snapshot,此时就算其他线程在操作 CopyOnWriteArrayList(add/remove等操作),也是在新数组上做操作,当前迭代器操作的对象数组不是同一个,就不会说当前迭代查询的时候突然数组多了或者少了成员。

三、总结

1. COW vs 读写锁

(1) 相同点

  • 两者都是通过读写分离的思想实现
  • 读线程间是互不阻塞的

(2) 不同点

对读线程而言,为了实现数据实时性,在写锁被获取后,读线程会等待或者当读锁被获取后,写线程会等待,从而解决“脏读”等问题。也就是说如果使用读写锁依然会出现读线程阻塞等待的情况。而 COW 则完全放开了牺牲数据实时性而保证数据最终一致性,即读线程对数据的更新是延时感知的,因此读线程不会存在等待的情况。

2.缺点

CopyOnWrite 容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题,所以在开发的时候需要注意一下。

(1) 内存占用问题

因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对 象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对 象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比 如说 200M 左右,那么再写入 100M 数据进去,内存就会占用 300M,那么这个时候很有可能造成频繁的 minor GC 和 major GC。

(2)数据一致性问题

CopyOnWrite 容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用 CopyOnWrite 容器。

List集合之CopyOnWriteArrayList CopyOnWriteArrayList 类图 CopyOnWriteArrayList 类的思想 CopyOnWrite 简称 COW,根据名字来看就是写入时复制。意思就是大家共同去访问一个资源,如果有人想要去修改这个资源的时候,就需要复制一个副本,去修改这个副本,而对于其他人来说访问的资源还是原来的,不会发生变化 CopyOnWriteArrayList 源码 CopyOnWriteArrayList 的变量 CopyOnWriteArrayList 底层是也是由数组实现的。本文我们只解读添加元素和读 阅读详情

相关推荐

CopyOnWriteArrayList原理,优缺点,使用场景

什么是CopyOnWrite容器   CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWri...

Top5软件工程硕士,先后在京东、字节从事多年Java后端开发、实时和离线大数据开发 4万+

CopyOnWriterArrayList 详解

在 ArrayList 的类注释上,JDK 就提醒了我们,如果要把 ArrayList 作为共享变量的话,是线程不安全的,推荐我们自己加锁或者使用 Collections.synchronizedList 方法,其实 JDK 还提供了另外一种线程安全的 List,叫做 CopyOnWriteArrayList。......

GeGe&YoYo的博客 1万+

从FreeCAD到BIMBase:PlaneGCS约束求解器的移植使用指南

本文为BIMBase平台的开发者提供了一份详细的PlaneGCS几何约束求解器移植与集成指南。文章深入探讨了如何从FreeCAD中提取PlaneGCS核心代码,并将其适配到BIMBase的C++二次开发环境中,重点解决了内存管理、数据模型桥接、约束抽象以及性能调优等关键问题,旨在帮助开发者构建稳定高效的参数化设计模块。

weixin_29213525的博客 321

CopyOnWriteArrayList详解

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33979363的博客 404

时序定江山,功耗写风骨——解读时序库lib文件的奥义【芯片设计 lib文件】

lib库,全称liberty library format file:它是Liberty时序文件的简称,使用 Liberty 语法来编写。它可以通过library compiler/dc转换成.db文件(database),供synopsis家的DC和PT使用,也可以通过spyglass的library compiler来生成sglib文件,供spyglass使用。他们是等价的,但db文件是二进制格式,人类不可读。

LogicYarn的博客 2603

7系列ZYNQ硬件调试:Vitis无法识别ARM DAP的排查与解决

本文详细分析了Xilinx 7系列ZYNQ芯片在Vitis开发环境中无法识别ARM DAP的常见问题及解决方案。从硬件设计检查(电源系统、时钟电路)、JTAG连接诊断到软件环境排查,提供了全面的调试指南。特别针对ARM DAP初始化失败的情况,给出了实用的排查步骤和预防措施,帮助开发者快速定位和解决类似问题。

weixin_33737134的博客 515

Java 中的 List 接口有哪些实现类?

理解各实现类的底层机制,能帮助开发者根据数据规模、操作类型和并发需求做出最优选择,从而提升系统性能与资源利用率。:百万级数据操作(JDK 17)

2302_81141145的博客 1368

【硬件设计】_基于STM32单片机的智能台灯系统

智能台灯系统是一款多功能的智能照明设备,能够自动调节亮度、监测人体活动、感知环境光照,并具备定时和报警功能,极大提升了用户体验与功能的灵活性,主要包括以下功能:系统提供自动和手动两种模式以调节台灯亮度。在自动模式下,台灯会根据周围光强智能调整;而在手动模式下,用户可通过按键自行设置亮度。利用红外光电开关检测台灯与用户之间的距离,若距离过近,系统将发出报警提示。通过时钟模块设置定时提醒,并在OLED显示屏上显示当前时间,当达到设定的定时时间时,蜂鸣器将发出报警音。

2401_85381181的博客 1732

List接口及常见实现类

List接口是 Java 集合框架中的一个重要组成部分,主要用于存储一组元素,这些元素可以是重复的,并且可以通过索引访问。List接口继承了Collection接口,而Collection接口又继承了Iterable接口。这种继承关系使得List接口具备了迭代的能力,即能够通过实现Iterable接口中的方法来遍历集合中的元素。

冷雪言的博客 1233

Synopys 验证系列-EDA 总结

VC Formal 解决方案包括一整套正式应用程序 (App),包括属性验证 (FPV)、自动提取属性 (AEP)、覆盖分析器 (FCA)、连接性检查 (CC)、时序等效性检查 (SEQ)、寄存器验证 (FRV)、测试平台分析仪 (FTA)、形式导航器 (NAV) 以及用于验证标准总线协议的一组断言 IP (AIP)。

qq_41289491的博客 1428

BAPI_OUTB_DELIVERY_CHANGE扩展指南:利用EXTENSION2与SMOD_V50B0001实现自定义字段更新

本文详细解析了如何利用BAPI_OUTB_DELIVERY_CHANGE的EXTENSION2参数与增强点SMOD_V50B0001,在不修改标准代码的前提下,安全、标准地更新SAP交货单的自定义字段。通过清晰的步骤指南和实战代码示例,帮助开发者掌握通过增强字段实现定制化业务需求的完整流程与调试技巧。

fanta的博客 451

Java中的List实现类详解

List是Java集合框架中最常用的接口之一,表示有序的集合(也称为序列)。Java提供了多种List接口的实现,每种实现都有其特定的使用场景和性能特点。

echo_123qq的博客 1195

Microsoft Dynamics 365软件二次开发:Dynamics365部署与版本控制策略

在Microsoft Dynamics 365中,自定义实体允许开发者扩展系统以满足特定业务需求。自定义实体可以包含字段、选项集、关系和其他属性,与标准实体类似,但完全由用户定义。1. 登录到Dynamics 365环境。2. 转到“解决方案”区域,选择“自定义”。3. 在“自定义”菜单中,选择“实体”。4. 点击“新建”,输入实体名称,例如“客户反馈”。5. 添加字段,如“反馈类型”、“反馈日期”、“描述”等。6. 设置主键和唯一标识符。7. 定义关系,如与“客户”实体的关联。

kkchenjj的博客 2208

52. List 接口与实现类

摘要: List 是 Java 集合框架中有序、可重复的序列接口,继承自 Collection。核心特性包括:有序存储(按插入顺序)、允许重复元素和 null 值、支持索引操作(如 get/set)。主要实现类有 ArrayList(随机访问快)、LinkedList(插入删除快)和 Vector(线程安全)。关键方法包含添加(add/addAll)、删除(remove/removeAll)、查询(get/indexOf)等。

weixin_43484713的博客 1048

零基础快速学习 ABAP 读者意见反馈和下一步写作计划表

大家在学习教程过程中,如果有任何关于 SAP ABAP 开发方面想了解的内容,可以直接在教程各篇文章下评论留言,我会将大家的意见统一汇总到下面的表格,作为我更新教程的内容参考来源之一。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 4273

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Java面试必备:Java 中的 List 接口及其实现类详解

List 接口是 Java 集合框架中最常用的接口之一,它继承自 Collection 接口,代表有序的集合(也称为序列)。List 允许重复元素,并且可以通过索引精确控制每个元素的插入位置。

qq_58299462的博客 1072

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Java中的List集合:从ArrayList到CopyOnWriteArrayList

在Java集合框架中,List接口是最常用的接口之一。List表示一个有序的集合,允许重复元素,并提供了基于索引的操作。add(E e): 添加元素到列表末尾: 在指定位置插入元素: 获取指定位置的元素: 移除指定位置的元素: 替换指定位置的元素size(): 返回列表的大小Java提供了多种List实现,每种实现都有其特定的用途和性能特征。

2303_76892351的博客 1542

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

2. Java 中的 List 接口有哪些实现类?

ArrayList: 适合频繁访问元素、不经常插入或删除的场景。LinkedList: 适合频繁插入、删除元素、不需要随机访问的场景。Vector: 一个线程安全的ArrayList,但性能较差,现代开发中较少使用。Stack: 用于LIFO栈结构,但推荐使用ArrayDeque或LinkedList替代。: 适合多线程读操作频繁的场景。这些实现类在不同的场景下具有不同的优势和劣势,选择合适的List实现类对于编写高效的代码非常重要。

zhzjn的博客 984

深入解析Java List接口及其实现类

在Java编程语言中,List接口是Java集合框架(Java Collections Framework)的一部分,它定义了有序集合的行为,允许我们对元素进行插入、删除和访问等操作。List中的每个元素都有一个精确的索引,从0开始,这使得我们可以准确地定位和操作集合中的任何一个元素。List接口的实现类有很多,其中最常见的是ArrayList和LinkedList。这两种实现类在内部结构和性能上有很大的不同,适用于不同的使用场景。

yokeyhui的博客 975

Java List 完全指南:从接口特性到四大实现类深度解析

ArrayList:性能均衡,适合大多数场景;LinkedList:适合频繁增删的场景;:适合并发读多写少的场景;Vector:尽量避免使用,仅兼容旧代码。选择时需根据 “访问效率、修改频率、并发场景” 三大维度权衡。

2201_75355172的博客 1046

java基础: Java List接口实现类深度解析:从底层原理到实战优化

List作为Java集合框架中最常用的接口之一,提供了有序、可重复的元素存储能力,其各类实现类在不同场景下展现出截然不同的性能特性。对于资深Java工程师而言,理解List接口实现类的底层设计、性能边界及适用场景,是编写高效代码的基础,也是大厂面试考察的核心能力。本文将系统解析List接口的主要实现类,从数据结构到实战应用,全方位呈现其技术细节。

weixin_43290370的博客 698

Java基础:Java List 接口实现类深度解析

在 Java 集合框架中,List 作为最常用的有序集合接口,其实现类的选择直接影响系统性能与内存效率。对于资深工程师而言,不仅要掌握各类实现的表层特性,更要理解其底层数据结构与适用场景的本质差异。本文将从体系架构、实战案例和深度原理三个维度,全面剖析 List 接口的核心实现类。

weixin_43290370的博客 454

深入解析 Java List 实现类的底层原理

在 Java 集合框架中,List接口是最常用的接口之一,其实现类在不同场景下表现各异。以下从数据结构、源码实现、性能特征及典型应用场景四个维度进行更深入的解析。

hqxstudying的博客 1025

“探索Java List的无限可能:从基础到高级应用“

什么是List?在集合框架中,List是一个接口,继承自Collection。Collection也是一个接口,该接口中规范了后序容器中常用的一些方法,具体如下所示List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。------list的官方文档二: List常见接口介绍:List接口是java.util包中的一个重要接口,用于表示一个有序的集合。List接口继承自Collection接口,并添加了一些用于操作列表元素的方法。

2301_80350265学无止尽5的博客 9134
上一篇: List接口实现类(2):LinkedList
下一篇: List接口实现类(4):Vector
nowseeme
博客等级 码龄10年 11粉丝 75原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值