集合系列(十五) -CopyOnWriteArrayList详解

一、摘要

在介绍 CopyOnWriteArrayList 之前,我们一起先来看看如下方法执行结果,代码内容如下:

public static void main(String[] args) {
    List<String> list = new ArrayList<String>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());
    //通过对象移除等于内容为1的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果内容如下:

原始list元素:[1, 2, 1]
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at com.example.container.a.TestList.main(TestList.java:16)

很遗憾,结果并没有达到我们想要的预期效果,执行之后直接报错!抛ConcurrentModificationException异常!

为啥会抛这个异常呢?

我们一起来看看,foreach 写法实际上是对List.iterator() 迭代器的一种简写,因此我们可以从分析List.iterator() 迭代器进行入手,看看为啥会抛这个异常。

ArrayList类中的Iterator迭代器实现,源码内容:

通过代码我们发现 ItrArrayList 中定义的一个私有内部类,每次调用nextremove方法时,都会调用checkForComodification方法,源码如下:

/**修改次数检查*/
final void checkForComodification() {
    //检查List中的修改次数是否与迭代器类中的修改次数相等
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

checkForComodification方法,实际上是用来检查List中的修改次数modCount是否与迭代器类中的修改次数expectedModCount相等,如果不相等,就会抛出ConcurrentModificationException异常!

那么问题基本上已经清晰了,上面的运行结果之所以会抛出这个异常,就是因为List中的修改次数modCount与迭代器类中的修改次数expectedModCount不相同造成的!

阅读过集合源码的朋友,可能想起Vector这个类,它不是 JDK 中 ArrayList 线程安全的一个版本么?

好的,为了眼见为实,我们把ArrayList换成Vector来测试一下,代码如下:

public static void main(String[] args) {
    Vector<String> list = new Vector<String>();
    //模拟10个线程向list中添加内容,并且读取内容
    for (int i = 0; i < 5; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加内容
                list.add(j + "-j");

                //读取内容
                for (String str : list) {
                    System.out.println("内容:" + str);
                }
            }
        }).start();
    }
}

执行程序,运行结果如下:

还是一样的结果,抛异常了Vector虽然线程安全,只不过是加了synchronized关键字,但是迭代问题完全没有解决!

继续回到本文要介绍的 CopyOnWriteArrayList 类,我们把上面的例子,换成CopyOnWriteArrayList类来试试,源码内容如下:

public static void main(String[] args) {
    //将ArrayList换成CopyOnWriteArrayList
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("1");
    list.add("2");
    list.add("1");
    System.out.println("原始list元素:"+ list.toString());

    //通过对象移除等于11的元素
    for (String item : list) {
        if("1".equals(item)) {
            list.remove(item);
        }
    }
    System.out.println("通过对象移除后的list元素:"+ list.toString());
}

执行结果如下:

原始list元素:[1, 2, 1]
通过对象移除后的list元素:[2]

呃呵,执行成功了,没有报错!是不是很神奇~~

当然,类似上面这样的例子有很多,比如写10个线程向list中添加元素读取内容,也会抛出上面那个异常,操作如下:

public static void main(String[] args) {
    final List<String> list = new ArrayList<>();
    //模拟10个线程向list中添加内容,并且读取内容
    for (int i = 0; i < 10; i++) {
        final int j = i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //添加内容
                list.add(j + "-j");

                //读取内容
                for (String str : list) {
                    System.out.println("内容:" + str);
                }
            }
        }).start();
    }
}

类似的操作例子就非常多了,这里就不一一举例了。

CopyOnWriteArrayList 实际上是 ArrayList 一个线程安全的操作类!

从它的名字可以看出,CopyOnWrite 是在写入的时候,不修改原内容,而是将原来的内容复制一份到新的数组,然后向新数组写完数据之后,再移动内存指针,将目标指向最新的位置。

二、简介

从 JDK1.5 开始 Java 并发包里提供了两个使用CopyOnWrite 机制实现的并发容器,分别是CopyOnWriteArrayListCopyOnWriteArraySet

从名字上看,CopyOnWriteArrayList主要针对动态数组,一个线程安全版本的 ArrayList !

CopyOnWriteArraySet主要针对集,CopyOnWriteArraySet可以理解为HashSet线程安全的操作类,我们都知道HashSet基于散列表HashMap实现,但是CopyOnWriteArraySet并不是基于散列表实现,而是基于CopyOnWriteArrayList动态数组实现!

关于这一点,我们可以从它的源码中得出结论,部分源码内容:

从源码上可以看出,CopyOnWriteArraySet默认初始化的时候,实例化了CopyOnWriteArrayList类,CopyOnWriteArraySet的大部分方法,例如addremove等方法都基于CopyOnWriteArraySet实现!

两者最大的不同点是,CopyOnWriteArrayList可以允许元素重复,而CopyOnWriteArraySet不允许有重复的元素!

好了,继续来 BB 本文要介绍的CopyOnWriteArrayList类~~

打开CopyOnWriteArrayList类的源码,内容如下:

可以看到 CopyOnWriteArrayList 的存储元素的数组array变量,使用了volatile关键字保证的多线程下数据可见行;同时,使用了ReentrantLock可重入锁对象,保证线程操作安全。

在初始化阶段,CopyOnWriteArrayList默认给数组初始化了一个对象,当然,初始化方法还有很多,比如如下我们经常会用到的一个初始化方法,源码内容如下:

这个方法,表示如果我们传入的是一个 ArrayList数组对象,会将对象内容复制一份到新的数组中,然后初始化进去,操作如下:

List<String> list = new ArrayList<>();
...
//CopyOnWriteArrayList将list内容复制出来,并创建一个新的数组
CopyOnWriteArrayList<String> copyList = new CopyOnWriteArrayList<>(list);

CopyOnWriteArrayList是对原数组内容进行复制再写入,那么是不是也存在多线程下操作也会发生冲突呢?

下面我们再一起来看看它的方法实现!

三、常用方法

3.1、添加元素

add()方法是CopyOnWriteArrayList的添加元素的入口!

CopyOnWriteArrayList之所以能保证多线程下安全操作, add()方法功不可没,源码如下:

操作步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、将原数组内容复制到新数组;
  • 4、写入数据;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

在 Java 中,独占锁方面,有2种方式可以保证线程操作安全,一种是使用虚拟机提供的synchronized 来保证并发安全,另一种是使用JUC包下的ReentrantLock可重入锁来保证线程操作安全。

CopyOnWriteArrayList使用了ReentrantLock这种可重入锁,保证了线程操作安全,同时数组变量array使用volatile保证多线程下数据的可见行!

其他的,还有指定下标进行添加的方法,如add(int index, E element),操作类似,先找到需要添加的位置,如果是中间位置,则以添加位置为分界点,分两次进行复制,最后写入数据!

3.2、移除元素

remove()方法是CopyOnWriteArrayList的移除元素的入口!

源码如下:

操作类似添加方法,步骤如下:

  • 1、获得对象锁;
  • 2、获取数组内容;
  • 3、判断移除的元素是否为数组最后的元素,如果是最后的元素,直接将旧元素内容复制到新数组,并重新设置array值;
  • 4、如果是中间元素,以index为分界点,分两节复制;
  • 5、将array数组变量地址指向新数组;
  • 6、释放对象锁;

当然,移除的方法还有基于对象的remove(Object o),原理也是一样的,先找到元素的下标,然后执行移除操作。

3.3、查询元素

get()方法是CopyOnWriteArrayList的查询元素的入口!

源码如下:

public E get(int index) {
    //获取数组内容,通过下标直接获取
    return get(getArray(), index);
}

查询因为不涉及到数据操作,所以无需使用锁进行处理!

3.4、遍历元素

上文中我们介绍到,基本都是在遍历元素的时候因为修改次数与迭代器中的修改次数不一致,导致检查的时候抛异常,我们一起来看看CopyOnWriteArrayList迭代器实现。

打开源码,可以得出CopyOnWriteArrayList返回的迭代器是COWIterator,源码如下:

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

打开COWIterator类,其实它是CopyOnWriteArrayList的一个静态内部类,源码如下:

可以看出,在使用迭代器的时候,遍历的元素都来自于上面的getArray()方法传入的对象数组,也就是传递进来的 array 数组!

由此可见,CopyOnWriteArrayList 在使用迭代器遍历的时候,操作的都是原数组,没有像上面那样进行修改次数判断,所以不会抛异常!

当然,从源码上也可以得出,使用CopyOnWriteArrayList的迭代器进行遍历元素的时候,不能调用remove()方法移除元素,因为不支持此操作!

如果想要移除元素,只能使用CopyOnWriteArrayList提供的remove()方法,而不是迭代器的remove()方法,这个需要注意一下!

四、总结

CopyOnWriteArrayList是一个典型的读写分离的动态数组操作类!

在写入数据的时候,将旧数组内容复制一份出来,然后向新的数组写入数据,最后将新的数组内存地址返回给数组变量;移除操作也类似,只是方式是移除元素而不是添加元素;而查询方法,因为不涉及线程操作,所以并没有加锁出来!

因为CopyOnWriteArrayList读取内容没有加锁,在写入数据的时候同时也可以进行读取数据操作,因此性能得到很大的提升,但是也有缺陷,对于边读边写的情况,不一定能实时的读到最新的数据,比如如下操作:

public static void main(String[] args) throws InterruptedException {
    final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    list.add("a");
    list.add("b");
    for (int i = 0; i < 5; i++) {
        final int j =i;
        new Thread(new Runnable() {
            @Override
            public void run() {
                //写入数据
                list.add("i-" + j);
                //读取数据
                for (String str : list) {
                    System.out.println("线程-" + Thread.currentThread().getName() + ",读取内容:" + str);
                }
            }
        }).start();
    }
}

新建5个线程向list中添加元素,执行结果如下:

可以看到,5个线程的读取内容有差异!

因此CopyOnWriteArrayList很适合读多写少的应用场景!

五、参考

1、JDK1.7&JDK1.8 源码

2、掘金 - 拥抱心中的梦想 - 说一说Java中的CopyOnWriteArrayList

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

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

相关文章

RabbitMQ(简单模式)

2种远程服务调用 1openFeign&#xff1a; 优点&#xff1a;能拿到被调用的微服务返回的数据&#xff0c;系统系耦度高&#xff0c;系统稳定。 缺点&#xff1a;同步调用&#xff0c;如果有很多服务需要被调用&#xff0c;耗时长。 MQ,消息队列&#xff0c;RabbitMQ是消息we…

windows DNS 服务器常用配置

Windows Server 上的 DNS 服务器配置主要包括以下步骤和常见设置&#xff1a; 安装 DNS 服务 打开服务器管理器&#xff0c;在 "角色和功能" 添加或删除角色中安装 DNS 服务器角色。 配置 DNS 服务器基础设置 添加 DNS 区域&#xff1a; 在 DNS 管理器中&#xff…

WPF 窗体样式 WindowStyle 设置 none, 如何移动窗体

当窗体样式设为None时&#xff0c;因为标题栏被隐藏&#xff0c;我们无法移动窗体&#xff0c;为了使窗体可以移动&#xff0c;我们要为window中的布局控件添加MouseDown事件的事件处理器&#xff0c;并且要为其设置一个背景(window设置为允许透明&#xff0c;当grid没有背景时…

书籍推荐|meta分析R语言实践教程-Doing Meta-Analysis with R: A Hands-On Guide

“The problems are solved, not by giving new information, but by arranging what we have known since long.” – Ludwig Wittgenstein 推荐理由 《Doing Meta-Analysis with R: A Hands-On Guide》是由 Mathias Harrer, Pim Cuijpers, Toshi Furukawa, 和 David Ebert所…

Hides for Mac:应用程序隐藏工具

Hides for Mac是一款功能强大的应用程序隐藏工具&#xff0c;专为Mac用户设计。它能够帮助用户快速隐藏当前正在运行的应用程序窗口&#xff0c;保护用户的隐私和工作内容&#xff0c;避免不必要的干扰。 软件下载&#xff1a;Hides for Mac下载 Hides for Mac的使用非常简单直…

Windows下载使用nc(netcat)命令

‘nc’ 不是内部或外部命令&#xff0c;也不是可运行的程序&#xff1f; 点击链接地址&#xff0c;下载压缩包。 完成后解压 使用方式&#xff08;三种&#xff09;&#xff1a; 1、直接双击exe使用 2、把这个exe放到cmd启动的默认路径下 放到默认路径下&#xff0c;使用nc&a…

0.96寸OLED屏调试 ----(一)

所需设备&#xff1a; 1、USB 转 SPI I2C 适配器&#xff1b;内附链接 2、0.96寸OLED显示模块&#xff1b; 备注&#xff1a;专业版、升级版都适用&#xff1b; 首先介绍一下OLED显示模块&#xff0c;SSD1306是一款OLED驱动芯片&#xff0c;拥有最大128*64像素支持&#xff…

亚马逊云科技:基于老服务器打造的旧实例类型

内容摘要&#xff1a; 2021年&#xff0c;距离第一个EC2实例上线已经十五周年了。 在漫长的开发过程中&#xff0c;很多EC2实例自然会基于旧服务器构建。 随着时间的推移&#xff0c;旧的服务器总是需要更换硬件&#xff0c;实例也得更换&#xff0c;但并不是所有的用户都想迁…

腾讯云免费云服务器申请流程详解

随着云计算的普及&#xff0c;越来越多的企业和个人开始选择使用云服务器。腾讯云作为国内领先的云计算服务提供商&#xff0c;为用户提供了丰富的云产品和服务。本文将为大家详细介绍腾讯云免费云服务器的申请流程。 一、注册腾讯云账号 首先&#xff0c;需要注册一个腾讯云账…

【氮化镓】镁激活退火对p-GaN迁移率和阈值电压的影响

【Mg activation anneal of the p-GaN body in trench gate MOSFETs and its effect on channel mobility and threshold voltage stability】 文献总结&#xff1a; 本研究探讨了在沟道栅MOSFETs中&#xff0c;镁&#xff08;Mg&#xff09;激活退火步骤对p型氮化镓&#xf…

python opencv稍基础初学

傅里叶变换 傅里叶变换f​​​​​傅里叶分析之掐死教程&#xff08;完整版&#xff09;更新于2014.06.06 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/19763358 相当nice 傅里叶变换的作用 高频&#xff1a;变化剧烈的灰度分量&#xff0c;例如边界 低频&#xff1a;变…

如何使用极狐GitLab 自定义 Pages 根域名

本文作者&#xff1a;徐晓伟 GitLab 是一个全球知名的一体化 DevOps 平台&#xff0c;很多人都通过私有化部署 GitLab 来进行源代码托管。极狐GitLab 是 GitLab 在中国的发行版&#xff0c;专门为中国程序员服务。可以一键式部署极狐GitLab。 本文主要讲述了极狐GitLab Pages …

【Ollama】AI大模型本地部署

Ollama Ollama轻量级、可扩展的框架&#xff0c;用于在本地机器上构建和运行大型语言模型&#xff08;LLM&#xff09;。提供了一个简单的API来创建、运行和管理模型&#xff0c;以及一个预构建模型库&#xff0c;可以轻松用于各种应用程序。 明显优势&#xff1a;易于使用、…

大模型prompt工程学习(一)

目录 调prompt的方法 prompt时好时不好 大模型本质是没有记忆的 划重点:我们发给大模型的 prompt&#xff0c;不会改变大模型的参数 ГLets think step by step」 一步步分析一下 自洽性&#xff0c;同时跑多次&#xff0c;来减少幻觉 逻辑&#xff0c;基本能力来是要有…

【数据结构】计数排序

大家好&#xff0c;我是苏貝&#xff0c;本篇博客带大家了解计数排序&#xff0c;如果你觉得我写的还不错的话&#xff0c;可以给我一个赞&#x1f44d;吗&#xff0c;感谢❤️ 目录 一. 基本思想二. 计数排序代码 一. 基本思想 计数排序又称为鸽巢原理&#xff0c;是对哈希直…

【爬虫基础】第6讲 opener的使用

在爬虫中&#xff0c;opener是一个用来发送HTTP请求的对象。它可以用来模拟浏览器发送请求&#xff0c;包括设置请求头、处理Cookie等操作。使用opener可以实现一些高级功能&#xff0c;如模拟登录、处理验证码等。 方法1&#xff1a; from urllib.request import Request,bu…

Axure中后台系统原型模板,B端页面设计实例,高保真高交互54页

作品概况 页面数量&#xff1a;共 50 页&#xff08;长期更新&#xff09; 兼容版本&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;网页模板、网站后台、中台系统、B端系统 作品特色 本品为「web中后台系统页面设计实例模板」&#xff0c;默林原创…

【搜索引擎2】实现API方式调用ElasticSearch8接口

1、理解ElasticSearch各名词含义 ElasticSearch对比Mysql Mysql数据库Elastic SearchDatabase7.X版本前有Type&#xff0c;对比数据库中的表&#xff0c;新版取消了TableIndexRowDocumentColumnmapping Elasticsearch是使用Java开发的&#xff0c;8.1版本的ES需要JDK17及以上…

StableDiffusion Web UI开启FP8,极大节约显存

升级了Pytorch后&#xff0c;StableDiffusion最新版本就可以有使用FP8的基础了&#xff0c;因此把秋叶的LINUX包也升级到了最新的版本。 升级Pytorch参考我的升级记录&#xff1a; ComfyUI SDWebUI升级pytorch随记-CSDN博客 然后下一步就是如何开启FP8了。与ComfyUI不同&…

【测试工具】JMeter接口测试的简单使用

事先声明&#xff1a;博主的JMeter是3.3版本的&#xff0c;可能和最新版本的操作有些许差别 测试前的准备工作 1、先添加一个线程组&#xff1a;右击“测试计划”&#xff0c;点击“添加”—》“Threads(Users)”—》“线程组” 2、再添加一个HTTP请求&#xff0c;右击“线程…