java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > CopyOnWriteArrayList使用详解

Java集合中的CopyOnWriteArrayList使用详解

作者:会飞的猪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使用详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文