高并发下你还敢用ArrayList?过来看看CopyOnWriteArrayList吧!

news2024/11/24 3:37:29

一、ArrayList线程不安全

在Java的集合框架中,想必大家对ArrayList肯定不陌生,单线程的情况下使用它去做一些CRUD的操作是非常方便的,先来看看这个例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        arrayList.add("a");
        arrayList.add("b");
        arrayList.add("c");
        for (String s : arrayList) {
            System.out.println(s);
        }
    }
}
复制代码

其输出结果就是与元素被添加进ArrayList的顺序一样,即:

a
b
c
复制代码

但是到了多线程的情况下,ArrayList还会像单线程一样执行结果符合我们的预期吗?我们再来看下这个例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new ArrayList<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}
复制代码

由输出结果:

Exception in thread "线程10" 
Exception in thread "线程1" 
Exception in thread "线程14"
Exception in thread "线程3" 
Exception in thread "线程5" 
Exception in thread "线程2"
Exception in thread "线程6" 
Exception in thread "线程21" 
Exception in thread "线程23" 
Exception in thread "线程28" 
Exception in thread "线程29" 
java.util.ConcurrentModificationException
复制代码

我们发现,多线程的情况下,有多个线程对ArrayList添加元素,同时又会有多个元素对ArrayList进行元素的读取,这样使得程序抛出了ConcurrentModificationException并发修改异常,所以我们可以下定结论:ArrayList线程不安全!

二、解决ArrayList线程不安全的方案

1、使用Vector类

我们知道,Java集合中的Vector类是线程安全的,可以用Vector去解决上述的问题。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = new Vector<>();
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}

复制代码

翻看Vector的源码可知,其add方法是使用了synchronized同步锁,同一时刻只允许一个线程对List进行修改。虽然Vector能够保证线程安全,但通过前面几期推文的学习,synchronized的方案一般不是最优选择,会对程序的性能有一定的影响。

2、使用Collections类

Collections类中的synchronizedList方法可以使一个线程不安全的List转为线程安全的。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}
复制代码

即使是不看synchronizedList的源码我们也可以通过它的名字猜到其底层也是使用synchronized来保证线程安全的,这也不是最优解。

3、使用CopyOnWriteArrayList类

CopyOnWriteArrayList类就是我们今天要主要探讨的重头。

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                arrayList.add(UUID.randomUUID().toString().substring(0,5));
                //往list中添加一个长为5的随机字符串
                System.out.println(arrayList);
                //读取list
            },"线程"+(i+1)).start();
        }
    }
}
复制代码

下面我们来聊聊CopyOnWriteArrayList是这么实现线程安全的。

三、CopyOnWriteArrayList

1、简介

java.util.concurrent包下的并发List只有CopyOnWriteArrayList。CopyOnWriteArrayList是一个线程安全的ArrrayList,对其进行的修改操作都是在底层的一个复制的数组上进行的,采用了写时复制,读写分离的思想。其类图结构如下:

通过类图可以清楚下面几点:

  • 每个CopyOnWriteArrayList对象中有一个array数组对象用来存放具体元素
  • ReentrantLock独占锁对象用来保证同一时刻只有一个线程对array进行修改

如果要我们自己来实现一个写时复制的线程安全的List,要考虑哪些点呢?

下面我们带着以下的问题与思考,来学习下CopyOnWriteArrayList吧!

  • 何时初试化list,初始化的list元素个数为多少,list的大小是是有限的吗?
  • 如何保证线程安全,多个线程对list进行读写时是如何保证线程安全的?
  • 如何确保使用迭代器变量list时的数据一致性?

2、主要方法源码分析

1、初始化

无参构造函数,其实是在内部创建了一个大小为0的Object数组作为array的初始值

 	public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
复制代码

有参构造函数有两个:

	public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
复制代码

CopyOnWriteArrayList中的array数组元素是入参toCopyIn数组元素的拷贝。

	public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }
复制代码

入参为集合时,将集合里的元素复制到CopyOnWriteArrayList中。

2、添加元素

CopyOnWriteArrayList中用来添加元素的方法有很多,原理均类似,故选取add(E e)方法来进行学习。

	 public boolean add(E e) {
	 	//(1)
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	//(2)
            Object[] elements = getArray();
            //(3)
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            //(4)
            setArray(newElements);
            return true;
        } finally {
        	//(5)
            lock.unlock();
        }
    }
复制代码

上述代码中,独占锁的思想非常值得学习。

  • 调用add方法的线程会首先执行代码(1)去获取独占锁,如果有多个线程都调用add方法时则只有一个线程会去获取到该锁,其他线程会被阻塞挂起直到锁被释放。
  • 一个线程获取到锁后,就保证了该线程添加元素的过程中其他线程不会对array数组进行修改。
  • 线程获取到所后执行代码(2)获取array,然后执行代码(3)复制array到一个新数组中,新数组的大小是array数组的大小+1,所以CopyOnWriteArrayList是无界的List,并把新的元素添加进新数组中。
  • 代码(4)使用新数组替换原数组,并在返回前执行(5)释放锁。由于加了锁,所以整个add的过程是原子性操作。

小结一下就是,添加元素时,线程先获取独占锁,然后复制原数组到新数组,给新数组添加元素,再把添加完元素后的新数组复制回原数组,最后释放锁返回。这就是所谓的写时复制。

3、获取指定位置元素

使用 E get(int index)获取下班为index的元素,如果元素不存在则抛出IndexOutOfBoundsException异常。

	public E get(int index) {
        return get(getArray(), index);
    }
    final Object[] getArray() {
        return array;
    }
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
复制代码

上述的代码中,当线程x调用get方法获取指定位置的元素时,需要分两步,首先获取array数组,然后通过下标访问指定位置的元素,这个过程是没有加锁同步的。假设这时候List的内容如图所示,里面有1、2、3的元素:

由于线程x调用get方法时是没有加锁的,这就可能导致线程x在获取完array数组之后、访问指定位置元素之前,另外一个线程y进行了remove操作,假设要删除元素1,remove操作首先或获取独占锁,然后进行写时复制,也就是复制一份当前array数组,然后在复制的数组里删除线程x要调用get方法访问的元素1,然后让array指向复制的数组。所以这个时候array之前指向的数组的引用计数为1而不为0,因为线程x还在使用它,这是线程x要访问指定位置的元素了,而它操作的数组就是线程B删除元素之前的数组。如下示意图: 在这里插入图片描述 虽然线程y删除了index处的元素,但是线程x还是能够读到index处的元素,这就是写时复制策略产生的弱一致性问题

4、修改指定元素

使用E set(int index, E element)方法修改list中指定元素的值时,如果指定位置元素不存在则抛出IndexOutOfBoundsException异常。

 	public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
复制代码

上述代码也是先获取独占锁,从而阻止其他线程对array数组进行修改,然后获取当前数组,调用get方法获取指定位置的元素,若指定位置的元素值与新值不一致,则创建新数组并复制元素,然后在新数组上修改元素值,再将array指向新数组;如果指定位置的元素值与新值一直,则为了保证volatile语义,还是要重新设置array,虽然其值为改变。

5、删除元素

这里介绍E remove(int index)方法。

	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)
                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();
        }
    }
复制代码

如上代码其实和新增元素的代码类似,首先获取独占锁以保证删除数据期间其他线程不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的数组,最后在返回前释放锁。

6、弱一致性的迭代器

在讲解什么是迭代器的弱一致性前,先看看下面的例子:

public class ListTest {
    public static void main(String[] args) {
        List<String> arrayList =new CopyOnWriteArrayList<>();
        arrayList.add("Hello");
        arrayList.add("HuYa");
        Iterator<String> iterator = arrayList.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
复制代码

输出如下:

Hello
HuYa
复制代码

iterator的hasNext方法用于判断集合中是否还有元素,next用于返回具体元素。 那么CopyOnWriteArrayList中迭代器的弱一致性又是啥意思?所谓弱一致性,是指返回迭代器后,其他线程对list的增删改对迭代器是不可见的。

  	public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    static final class COWIterator<E> implements ListIterator<E> {
        //array的快照版本
        private final Object[] snapshot;
        //数组下标
        private int cursor;
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
    }
复制代码

上述代码中,当CopyOnWriteArrayList对象调用iterator()方法获取迭代器时实际上会返回COWIterator对象,COWIterator对象的snapshot变量保存了当前list的内容, cursor是遍历list时数据的下标。

特别对snapshot快照进行一个说明:

  • 如果在一个线程使用list返回的迭代器遍历元素的过程中,其他线程没有对list进行修改,那么snapshot本身就是list的array了。
  • 如果在一个线程使用list返回的迭代器遍历元素的过程中,其他线程对list进行修改,那么snapshot就是array的一个快照了,因为修改后list里面的数组其实是被新数组替换了,这时候原来的数组是被snapshot继续引用。

这也说明一个线程获取了迭代器后,使用该迭代器元素时,其他线程对该list进行的修改是不可见的,因为它们操作的是两个不同的数组,这也就是弱一致性。

最后再来看看一个例子:

public class ListTest {
    public static void main(String[] args) throws InterruptedException {
        List<String> arrayList =new CopyOnWriteArrayList<>( );
        arrayList.add("Hello");
        arrayList.add("HuYa");
        arrayList.add("Welcome");
        arrayList.add("to");
        arrayList.add("Guangzhou");

        Thread thread = new Thread(() -> {
            arrayList.set(1, "WeiXin");
            arrayList.remove(2);
            arrayList.remove(3);
        });

        //保证在修改线程启动前先获取迭代器
        Iterator<String> iterator = arrayList.iterator();
        Thread.sleep(1000);

        //修改线程启动
        thread.start();
        //等待修改执行完毕
        thread.join();

        //迭代元素
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}
复制代码

输出结果如下:

Hello
HuYa
Welcome
to
Guangzhou

复制代码

虽然说子线程对arrayList进行了修改,但是主线程在arrayList被修改之前先获取到了其迭代器,拿到了原来数组的快照,所以子线程的修改对于主线程使用迭代器进行迭代并没有影响。这就体现了CopyOnWriteArrayList迭代器的弱一致性。

四、总结

本期主要学习了CopyOnWriteArrayList一些主要方法的源码、思想,总结一下:

  • 最主要的是它写时复制的策略,来保证List的一致性,获取、修改、写入三步操作不是原子性的,故在增删改的过程中都使用了独占锁,来保证同一时刻只能有一个线程对list进行修改。
  • CopyOnWriteArrayList提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改对于迭代器是不可见的。
  • 综合CopyOnWriteArrayList上述的特性,它适用于读多写少的高并发场景。

但是CopyOnWriteArrayList也有缺点,开发时要注意一下:

  • 内存占用问题。因为CopyOnWriteArrayList的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,即旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,这个时候很有可能造成频繁的GC,应用响应时间也随之变长。
  • 数据一致性问题。CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性。

本期的学习就到这里, 我是Zhongger,一个在互联网行业摸鱼写代码的打工人,求个【关注】和【在看】,你们的支持是我创作的最大动力,我们下期见~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GIS基于智慧城市建设的作用

​​​​​智慧城市的建设对于改善居民的生活质量和提高城市的管理水平&#xff0c;有着公认的推动作用。其中&#xff0c;地理信息技术特别是GIS平台&#xff0c;在智慧城市的建设过程中扮演着关键角色。 在现实情况中&#xff0c;除了政策本身的一些因素&#xff0c;受限于一…

Web前端105天-day44-JSCORE

JSCORE04 目录 前言 一、复习 二、forEach 三、reduce 四、展开语法 五、解构语法 六、形参默认值 七、剩余参数 总结 前言 JSCORE04学习开始 一、复习 JS的第6个版本, 带来了大量的新特性, 新语法let/const : 两个新的声明变量的方式 新的作用域 脚本: 对应全局, 用…

RocketMQ中的线程池是如何创建的?

前言 大家好&#xff0c;今天主要来和大家聊一聊RocketMQ中的线程池是如何创建的&#xff0c;如何设置线程池数量&#xff0c;同时也可以从中去学习到一些线程池的实践和需要注意的一些细节。 RocketMQ在哪些地方使用到了线程池&#xff1f; 在RocketMQ中存在了大量的对线程…

学籍信息网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 学籍信息管理&#xff1a;添加信息、修改信息、删除信息、查询信息 添加信息&#xff0c;管理员根据学生的将信息导入系…

[附源码]Python计算机毕业设计高校师资管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

行业分析| 智慧头盔在快对讲上的应用与实践

快对讲综合调度系统是基于移动通信网络&#xff0c; 整合集群对讲、视频监控、实时音视频技术、PSTN、GIS定位、IM和调度业务的产品&#xff0c;为客户提供专业对讲、视频会议、可视化融合指挥调度等功能为一体的音视频实时交互平台。 快对讲和智慧头盔 智慧头盔&#xff0c;…

PHP实验室管理系统mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP实验室管理系统 是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为PHP APACHE&#xff0c;数据库为 mysql5.0&#xff0c;使用php语言开发。 PHP…

第10讲:vue脚手架集成axios

一、创建项目并添加axios支持 创建项目请参考&#xff1a;使用脚手架创建vue项目 创建路由项目请参考&#xff1a;路由开发 1.1、添加axios支持 使用如下命令添加axios支持 npm install axios //vue-cli2.0安装方式1.2、在main.js中引用并使用axios 使用如下命令 impor…

git初识(三)

分支 顾名思义&#xff0c;分支就是从主线上分离出来进行另外的操作&#xff0c;而又不影响主线&#xff0c;主线又可以继续干它的事&#xff0c;&#xff0c;最后分支做完事后合并到主线上而分支的任务完成可以删掉了。为了不受其他开发人员的影响&#xff0c;你可以在主分支…

数据看板可视化

前言 这段时间一直在做可视化&#xff0c;在我的项目中有一部分是电力巡检的数据可视化。其中的数据看板比较简单&#xff0c;我将其单独抽离出来形成一个demo&#xff0c;为保密demo中数据非真实数据。先看效果。 具体效果 链接相关 浏览链接&#xff1a;http://xisite.top…

【人工智能与机器学习】——聚类(学习笔记)

&#x1f4d6; 前言&#xff1a;我们之前学习的算法均是有监督学习&#xff08;supervised learning&#xff09;&#xff0c;它的一个鲜明特征是通过给定的标签来学习从数据特征&#xff08;如图像&#xff09;到语义标签的映射关系。但在很多实际问题中&#xff0c;数据并没有…

vuex笔记

Vuex Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 调试工具&#xff1a;vue devtools Vuex就像眼镜&#xff1a;您自会知道什么时候需要它。 1、state 在store中定义数据&#xff0c;在组件中直接使用&#xff1a; 目录&#xff1a;store/index.js export defau…

相关分析与回归分析

相关与回归分析就是了解变量之间相关关系的统计方法 一.相关分析 具有相关关系的变量之间&#xff0c;如果不区分原因和结果&#xff0c;我们称之为相关分析 相关分析是看两个因素之间的相关性&#xff0c;不需要确定哪个是自变量&#xff0c;哪个是因变量&#xff0c;两个因…

RK3568 GT911触摸屏调试

屏幕规格书 需要主要硬件通信电压为&#xff1a;1.8V或者3.3V I2C通信的地址&#xff1a;0x5D 和0x40 系统上电时序&#xff1a;不同的地址&#xff0c;稍微有些差异 对应代码中如下&#xff1a; 与RK3568的硬件接口电路 DTS 配置 驱动&#xff1a;RK自带的驱动程序就可以正确工…

音视频学习 -- 弱网对抗技术相关实践

背景介绍 实时音视频通话在当前的生活中是无时不刻存在的&#xff0c;包括社交、安防、交通等等各个方面都需要。用户场景复杂多变、要求严苛、网络环境不一致等给实时音视频通话带来很大条件。我们在这方向稍微做了一些工作&#xff0c;虽然和其他大厂的优化工作相比&#xf…

Commons Collections3

省流 SerialKiller 可以通过⿊名单与⽩名单的⽅式来限制反序列化时允许通过的 类&#xff0c;其中限制了cc1和cc2中命令执行的类&#xff0c;InvokerTransformer cc3就是为了绕过对其的限制&#xff0c;这里使用的是com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter来…

基于改进的DBN降水预测方法(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 精确高效的降水预测模型可以更好地反映未来的气候&#xff0c;为管理决策提供重要参考&#xff0c;帮助人们为未来的恶劣天气做…

首个元宇宙国家?!# Tuvalu

当我们还在谈论如何设计和构建一个未来城市的时候&#xff0c;首个入驻元宇宙的国家也诞生了。太平洋岛国「图瓦卢」开始 在虚拟世界创建一个数字国家。这是个怎样的国家&#xff1f;图瓦卢是位于南太平洋的一个岛国&#xff0c;人口约为 1.2 万&#xff0c;由 9 个环形小珊瑚岛…

FKM规范静强度和疲劳强度计算分析

1. 概述 WB-FKM/WB-FKM-Weld工具包是德国CADFEM公司基于FKM规范&#xff08;德国机械协会主持和通过的机械产品强度评估规范&#xff09;的基础上&#xff0c;在ANSYS WB内开发的针对结构母材及焊缝进行静强度和疲劳强度评估的工具包。 该工具包的最大优势是&#xff1a;基于AN…

艾美捷CpG ODN系列——ODN 2006 (TLRGRADE)说明

艾美捷CpG ODN系列——ODN 2006 (TLRGRADE)&#xff1a;具有硫代磷酸酯骨架的CpG寡脱氧核苷酸&#xff08;B型&#xff09;。人和小鼠TLR9&#xff08;Toll样受体9&#xff09;的特异性配体。 艾美捷CpG ODN 丨ODN 2006 (TLRGRADE)化学性质&#xff1a; 序列&#xff1a;5-tcg…