微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Java---CopyOnWriteArrayList详解


copyOnWriteArrayList是ArrayList的线程安全版本,从他的名字可以推测。copyOnWriteArrayList是在有写操作的时候会copy一份数据,然后写完再设置成新的数据。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));
    }
}

代码结果:

 

 

 

原文地址:https://www.jb51.cc/wenti/3279858.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐