Java集合中的CopyOnWriteArrayList使用详解

 更新时间:2023年12月19日 10:10:06   作者:会飞的猪zhu  
这篇文章主要介绍了Java集合中的CopyOnWriteArrayList使用详解,CopyOnWriteArrayList是ArrayList的线程安全版本,从他的名字可以推测,CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据,需要的朋友可以参考下

前言

CopyOnWriteArrayList是ArrayList的线程安全版本,从他的名字可以推测。CopyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。

CopyOnWriteArrayList

CopyOnWriteArrayList适用于读多写少的并发场景。

上面的图片展示你了CopyOnWriteArrayList的类图,可以看到它实现了List接口,如果去看ArrayList的类图的话,可以发现也是实现了List接口,也就得出一句废话,ArrayList提供的api,CopyOnWriteArrayList也提供,下文中来分析CopyOnWriteArrayList是如何来做到线程安全的实现读写数据的,而且也会顺便对比ArrayList的等效实现为什么不支持线程安全的。

下面首先展示了CopyOnWriteArrayList中比较重要的成员:

     /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();
    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

可以看到,CopyOnWriteArrayList使用了ReentrantLock来支持并发操作,array就是实际存放数据的数组对象。ReentrantLock是一种支持重入的独占锁,任意时刻只允许一个线程获得锁,所以可以安全的并发去写数组,接下来看一下CopyOnWriteArrayList是如何使用这个lock来实现并发写的,下面首先展示了add方法的代码:

public boolean add(E e) {
        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,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。

所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。 

他的缺点:

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

如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。

数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】

CopyOnWriteArrayList读取时不加锁,只是写入、删除、修改时加锁,所以一个线程X读取的时候另一个线程Y可能执行remove操作。

remove操作首先要获取独占锁,然后进行写时复制操作,就是复制一份当前的array数组,然后在复制的新数组里面删除线程X通过get访问的元素,比如:1。删除完成后让array指向这个新的数组。 在线程x执行get操作的时候并不是直接通过全局array访问数组元素而是通过方法的形参a访问的,a指向的地址和array指向的地址在调用get方法的那一刻是一样的,都指向了堆内存的数组对象。之后改变array指向的地址并不影响get的访问,因为在调用get方法的那一刻形参a指向的内存地址就已经确定了,不会改变。所以读的仍然是旧数组。

对Arrays.copyOf的认识

 首先Arrays.copyOf(Object[] , length),相对于数组来说,是深拷贝,但是相对于数组元素来说,只有数组为一维数组,并且元素为基本类型、包装类、String类为深拷贝,其他都为浅拷贝(针对的是数组元素)。

代码实例:

public class Test{
   static  class Person{
        int age ;
        String name;
        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
       @Override
       public String toString() {
           return "Person{" +
                   "age=" + age +
                   ", name='" + name + '\'' +
                   '}';
       }
   }
    public static void main(String[] args) {
       Person[] people = new Person[2];
       Person person1 = new Person(45,"dsad");
       Person person2 = new Person(105,"zhuzhu");
       people[0] = person1;
       people[1] = person2;
        Person[] people1 = Arrays.copyOf(people,people.length+1);
        System.out.println(Arrays.toString(people));
        System.out.println(Arrays.toString(people1));
      // 两者谁都可以改变,所以可以看出来,这个复制只是引用的复制,
     //而真正的对象其实还是同一个。
        people[0].age = 456;
        System.out.println("---------");
        System.out.println(Arrays.toString(people));
        System.out.println(Arrays.toString(people1));
    }
}

运行结果: 

这里其实并没有把具体的对象复制,而是复制了对象的引用而已。如果我们使用的是int[] 类型的数组,那么就会改变了

代码演示:

public class Test{
   static  class Person{
        int age ;
        String name;
        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
       @Override
       public String toString() {
           return "Person{" +
                   "age=" + age +
                   ", name='" + name + '\'' +
                   '}';
       }
   }
    public static void main(String[] args) {
       int[] arr1 = {4,5,6};
        int[] ints = Arrays.copyOf(arr1, arr1.length);
        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(ints));
        System.out.println("--------------");
        arr1[0] = 12;
        System.out.println(Arrays.toString(arr1));
        System.out.println(Arrays.toString(ints));
    }
}

运行结果:

可以直观的看到,一个数组变换了,另一个没有变换。 

下面这个相当于是一个set(index,val)源码方法的一个易懂的方式:

public class Test{
   static  class Person{
        int age ;
        String name;
        public Person(int age, String name) {
            this.age = age;
            this.name = name;
        }
       @Override
       public String toString() {
           return "Person{" +
                   "age=" + age +
                   ", name='" + name + '\'' +
                   '}';
       }
   }
    public static void main(String[] args) {
       Person[] people = new Person[2];
       Person person1 = new Person(45,"dsad");
       Person person2 = new Person(105,"zhuzhu");
       people[0] = person1;
       people[1] = person2;
        Person[] people1 = Arrays.copyOf(people,people.length+1);
        System.out.println(Arrays.toString(people));
        System.out.println(Arrays.toString(people));
        people1[0] = new Person(45777,"456");
        System.out.println(Arrays.toString(people));
        System.out.println(Arrays.toString(people1));
        people = people1;
        System.out.println(Arrays.toString(people));
        System.out.println(Arrays.toString(people1));
    }
}

代码结果:

到此这篇关于Java集合中的CopyOnWriteArrayList使用详解的文章就介绍到这了,更多相关CopyOnWriteArrayList使用详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java利用蒙特卡洛方法求解圆周率π值

    Java利用蒙特卡洛方法求解圆周率π值

    蒙特·卡罗方法(Monte Carlo method),也称统计模拟方法,是一种以概率统计理论为基础的数值计算方法。本文将利用该方法实现圆周率的计算,需要的可以参考一下
    2022-08-08
  • IDEA自定义setter和getter格式的设置方法

    IDEA自定义setter和getter格式的设置方法

    这篇文章主要介绍了IDEA自定义setter和getter格式的设置方法,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧
    2023-12-12
  • SpringCloud使用CircuitBreaker实现熔断器的详细步骤

    SpringCloud使用CircuitBreaker实现熔断器的详细步骤

    在微服务架构中,服务之间的依赖调用非常频繁,当一个下游服务因高负载或故障导致响应变慢或不可用时,可能会引发上游服务的级联故障,最终导致整个系统崩溃,熔断器是解决这类问题的关键模式之一,Spring Cloud提供了对熔断器的支持,本文将详细介绍如何集成和使用它
    2025-02-02
  • Maven setting配置镜像仓库的方法步骤

    Maven setting配置镜像仓库的方法步骤

    这篇文章主要介绍了Maven setting配置镜像仓库的方法步骤,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-12-12
  • JDK1.8安装与配置超详细教程

    JDK1.8安装与配置超详细教程

    JDK1.8即为JDK8,JDK8是目前是最成熟最稳定的版本,本文将详细介绍JDK1.8的安装与配置,需要的朋友可以参考下
    2023-03-03
  • SpringBoot关闭过程中销毁DisposableBean解读

    SpringBoot关闭过程中销毁DisposableBean解读

    这篇文章主要介绍了SpringBoot关闭过程中销毁DisposableBean解读,一个bean的生命周期,指的是 bean 从创建,初始化,一系列使用,销毁的过程,今天来讲讲 bean 的初始化和销毁的方法,需要的朋友可以参考下
    2023-12-12
  • Spring Bean配置方式总结

    Spring Bean配置方式总结

    定义Spring Bcan的3种方式分别是:基于XML 的方式配置、基于注解扫播方式配置、基于元数据类的配置,本文就通过代码示例给大家详细讲讲这三种配置方式,需要的朋友可以参考下
    2023-12-12
  • 详细了解MVC+proxy

    详细了解MVC+proxy

    Java有两种代理方式,一种是静态代理,另一种是动态代理。对于静态代理,其实就是通过依赖注入,对对象进行封装,不让外部知道实现的细节。很多 API 就是通过这种形式来封装的
    2021-07-07
  • Mybatis实现增删改查(CRUD)实例代码

    Mybatis实现增删改查(CRUD)实例代码

    MyBatis 是支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架。通过本文给大家介绍Mybatis实现增删改查(CRUD)实例代码 ,需要的朋友参考下
    2016-05-05
  • Java自带的加密类MessageDigest类代码示例

    Java自带的加密类MessageDigest类代码示例

    这篇文章主要介绍了Java自带的加密类MessageDigest类代码示例,分享了常见的三种加密方式代码示例,具有一定参考价值,需要的朋友可以了解下。
    2017-11-11

最新评论