CopyOnWriteArrayList原理

CopyOnWriteArrayList原理

1. 简介

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

2. 原理

很多时候,我们的系统应对的都是读多写少的并发场景。CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。

  • 线程安全的,多线程环境下可以直接使用,无需加锁;

  • 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全;

  • 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去。
    在这里插入图片描述
    从整体架构上来说,CopyOnWriteArrayList 数据结构和 ArrayList 是一致的,底层是个数组,只不过 CopyOnWriteArrayList 在对数组进行操作的时候,基本会分四步走:

  • 加锁;

  • 从原数组中拷贝出新数组;

  • 在新数组上进行操作,并把新数组赋值给数组容器;

  • 解锁

除了加锁之外,CopyOnWriteArrayList 的底层数组还被 volatile 关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到,代码如下:

private transient volatile Object[] array;

整体上来说,CopyOnWriteArrayList 就是利用锁 + 数组拷贝 + volatile 关键字保证了 List 的线程安全。

3. 优点

读操作(不加锁)性能很高,因为无需任何同步措施,比较适用于读多写少的并发场景。Java的list在遍历时,若中途有别的线程对list容器进行修改,则会抛ConcurrentModificationException异常。而CopyOnWriteArrayList由于其"读写分离"的思想,遍历和修改操作分别作用在不同的list容器,所以在使用迭代器进行遍历时候,也就不会抛出ConcurrentModificationException异常了。

4. 缺点

一是内存占用问题,毕竟每次执行写操作都要将原容器拷贝一份。数据量大时,对内存压力较大,可能会引起频繁GC;

二是无法保证实时性,因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。

5. 源码分析

5.1 添加操作

	public boolean add(E e) {
        //ReentrantLock加锁,保证线程安全
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //拷贝原容器,长度为原容器长度加一
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新副本上执行添加操作
            newElements[len] = e;
            //将原容器引用指向新副本
            setArray(newElements);
            return true;
        } finally {
            //解锁
            lock.unlock();
        }
    }

添加的逻辑很简单,先将原容器copy一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。

5.2 删除操作

	public E remove(int index) {
        //加锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                //如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                //否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            //解锁
            lock.unlock();
        }
    }

删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。

我们再来看看读操作,CopyOnWriteArrayList的读操作是不用加锁的,性能很高。

	public E get(int index) {
        return get(getArray(), index);
    }
 	private E get(Object[] a, int index) {
        return (E) a[index];
    }

直接读取即可,无需加锁

5.3 弱一致性的迭代器

所谓弱一致性是指返回迭代器后,其他线程对list的增删改查对迭代器是不可见的

// 演示多线程下迭代器的弱一致性结果
public class copylist {
    private static volatile CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
    public static void main(String[] args) throws InterruptedException {
        arrayList.add("hello");
        arrayList.add("alibaba");
        arrayList.add("welcome");
        arrayList.add("to");
        arrayList.add("hangzhou");
 
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                // 修改list中下标为1的元素为ali
                arrayList.set(1, "ali");
                // 删除元素
                arrayList.remove(2);
                arrayList.remove(3);
            }
        });
        // 保证在修改线程启动前获取迭代器
        Iterator<String> itr = arrayList.iterator();
        // 启动线程
        threadOne.start();
        // 等待子线程执行完毕
        threadOne.join();
        while(itr.hasNext()) {
            System.out.println(itr.next());
        }
    }
}

hello
alibaba
welcome
to
hangzhou
 
Process finished with exit code 0

从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。需要注意的是,获取迭代器的操作必须在子线程操作之前进行。

6. ArrayList转为线程安全的方法

List list = Collections.synchronizedList(new ArrayList());

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/490109.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【解决】E: 无法获取 dpkg 前端锁 (/var/lib/dpkg/lock-frontend)

常用两个方法 1 杀死之前的相关进程 在提示信息里面有进程号 $ sudo kill 2158 2 强制解锁 sudo rm /var/cache/apt/archives/lock sudo rm /var/lib/dpkg/lock 以上不行时候&#xff0c;更新软件库 sudo apt-get update

锁车锁电曝视频+画像车主:车企的「科技与狠活」

作者 | 辰纹 来源 | 洞见新研社 近日&#xff0c;不少车企远程锁车锁电再度引热议。但车企的“科技狠活”没有最狠只有更狠&#xff0c;仅去年就发生数宗车企泄露车主视频等隐私数据的案例&#xff0c;不仅令当事车主“社死”&#xff0c;甚至成千上万网友发问“自己的车&…

2024中国闪存市场观察:AI助推闪存全面起势?

过去两年&#xff0c;闪存市场一直处于低迷状态&#xff0c;但去年第四季度闪存颗粒资源的上涨&#xff0c;导致闪存产品价格一路上扬&#xff0c;市场遂发生反转。 2024年&#xff0c;中国闪存市场会彻底走向复苏&#xff0c;还是急转直下&#xff1f;中国AI热潮&#xff0c;…

如何撰写高质量渗透测试报告

渗透测试作为信息安全领域的重要环节&#xff0c;其成果的体现形式往往凝聚在最终的渗透测试报告之中。一份优秀的渗透测试报告不仅记录了测试过程的每一个细节&#xff0c;更是指导客户改进安全状况、防范潜在风险的重要依据。下面&#xff0c;我们将深入探讨如何撰写一份详尽…

【最新!红外小目标检测算法HCFNet】

文章目录 摘要1 引言2 相关工作2.1 传统方法2.2 深度学习方法 3 方法3.1 PPA3.2 维度感知选择性整合模块3.3 多稀释通道细化器模块3.4 损失函数设计 4 实验4.1 数据集与评估指标4.2 实现细节4.3 消融和对比 5 结论 论文&#xff1a;HCF-Net: Hierarchical Context Fusion Netwo…

Python篇之网络编程,实现简单的服务端和客户端的内容传输

本小节内容&#xff1a;实现简单的信息交流 文章目录 一、 Socket介绍二、客户端与服务端三、在python中实现网络通信1. 服务端2. 客户端3. 连接测试 一、 Socket介绍 socket (简称 套接字) 模块是其内置的标准库之一&#xff0c;它实现了BSD sockets API&#xff0c;允许开发…

FreeRTOS使用记录

FreeRTOS使用记录 移植STM32F104ZGT6main.c 增加us延时 移植 STM32F104ZGT6 FreeRTOSConfig.h #ifndef FREERTOS_CONFIG_H #define FREERTOS_CONFIG_H/*-----------------------------------------------------------* Application specific definitions.** These definition…

ES 进阶知识

索引Index 一个索引就是一个拥有几分相似特征的文档的集合。比如说&#xff0c;你可以有一个客户数据的索引&#xff0c;另一个产品目录的索引&#xff0c;还有一个订单数据的索引。一个索引由一个名字来标识&#xff08;必须全部是小写字母&#xff09;&#xff0c;并且当我们…

C++ STL - 优先级队列及其模拟实现

目录 0. 引言 1. priority_queue 介绍 1.1 构造函数 1.2 priority_queue 接口函数使用 1.3 仿函数 1.4 题目练习 2. priority_queue 模拟实现 2.1基本框架&#xff1a; 2.2 默认构造函数 2.3 基本函数 2.4 堆的向上以及向下调整 0. 引言 优先队列 (priority_queu…

【测试开发学习历程】认识Python + 安装Python

目录 1 认识 Python 1.1 Python 的起源 1.2 Python的组成 1.2.1 解释器 1.1.2 Python 的设计目标 1.1.3 Python 的设计哲学 1.2 为什么选择 Python 测试人员选择Python的理由 1.3 Python 特点 面向对象的思维方式 1.4 Python 的优缺点 1.4.1 优点 1.4.2 缺点 3. 安…

Unity编辑器功能将AB资源文件生成MD5码

将路径Application.dataPath/ArtRes/AB/PC文件夹下所有的Ab包文件生成MD5吗&#xff0c;通过文件名 文件长度MD5‘|’的格式拼接成字符串写入到资源对比文件abCompareInfo.txt中。 将路径pathFile扥文件生成MD5码

vue项目在本地源码方式启动和打包之后在nginx中代理有什么不同

Vue项目在本地源码方式启动和打包之后在Nginx中代理的主要区别在于开发环境与生产环境的配置、性能优化、安全性和部署流程等方面。以下是一些具体的差异点&#xff1a; 开发环境与生产环境&#xff1a; 本地源码启动通常是在开发环境中&#xff0c;使用Vue CLI的vue-cli-servi…

关于在forEach循环中使用异步,造成forEach里面的函数还未执行完毕,外层的同步已经被执行的问题

使用 原生的 for循环替代forEach循环即可解决问题 1.实例代码&#xff1a; select_Father_comment_sql_res.forEach( (item) > {const Select_FId_children_sql util.format("Select *, \IFNULL(User.UserName,) as CommentUserName, \IFNULL(User.UserName,) as AtU…

【王道训练营】第3题 判断某个年份是不是闰年,如果是闰年,请输出“yes”,否则请输出“no”

文章目录 引言闰年初始代码代码改进改进1&#xff1a;添加提示信息改进2&#xff1a;代码格式改进3&#xff1a;变量命名 其他实现方式使用if-else语句使用函数使用三元操作符 结论 引言 在公历中&#xff0c;闰年的规则如下&#xff1a;如果某个年份能被4整除但不能被100整除…

pycharm使用远程服务器的jupyter环境

1、确保服务器上安装了jupyter,如果没有&#xff0c;执行下面命令安装 pip install jupyter2、启动jupyter notebook服务 jupyter notebook --no-browser --port8888 --ip0.0.0.0 --allow-root表明在服务器的8888 端口上启动 Jupyter Notebook&#xff0c;并允许从任何 IP 地…

qt事件机制学习笔记

实现闹钟功能 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget), speecher(new QTextToSpeech(this)) //给语音播报者实例化空间 {ui->setupUi(this); }Widget::~Widget() {delete …

CMake学习笔记(一)一个最简单的CMakeLists嵌套示例

目录 1 mkdir project_macro 2 在project_marco中建立CMakeLists.txt 3 建立专门的src文件夹 4 在src中添加main.cpp和CMakeLists.txt 5 回到project_macro目录&#xff0c;建立build文件夹 6 进入build 文件夹&#xff0c;开始cmake 7 在build文件夹里执行make指令 8 …

Vue.js 安装

1、独立版本 我们可以在 Vue.js 的官网上直接下载 vue.min.js 并用 <script> 标签引入。 2、使用 CDN 方法 以下推荐国外比较稳定的两个 CDN&#xff0c;国内还没发现哪一家比较好&#xff0c;目前还是建议下载到本地。 Staticfile CDN&#xff08;国内&#xff09; :…

uniapp开发H5页面如何打开调试 (vConsole)

前言&#xff1a; H5页面没有微信小程序那样的直接打开调试工具的功能&#xff0c;需要手动安装引用。步骤如下&#xff1a; 一、安装vConsole npm install vconsole 二、引用vConsole 在main.js文件中引入使用 import Vconsole from vconsole let vConsole new Vconsole()…

深度学习的发展历史(深度学习入门、学习指导)

目录 &#x1f3c0;前言 ⚽历史 第一代神经网络&#xff08;1958-1969&#xff09; 第二代神经网络&#xff08;1986-1998&#xff09; 统计学习方法的春天&#xff08;1986-2006&#xff09; 第三代神经网络——DL&#xff08;2006-至今&#xff09; &#x1f3d0;总结…