CopyOnWriteArrayList

作者: 何甜甜在吗 | 来源:发表于2018-01-01 14:57 被阅读1次

    今天来复习一下集合。在支持并发的集合中,我觉得CopyOnWriteArrayList是相对容易理解的一个。
    CopyOnWrite:写时复制,就是当有线程向集合添加元素时,不是直接往旧的容器中添加元素,而是将旧的容器中的元素复制到新的容器中,读的时候读的仍然是旧容器,这样就不会影响并发读了
    分析一下CopyOnWriteArrayList部分源码
    1.add(E e)方法,向容器中添加元素

    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();
            }
        }
    

    在往容器中加元素的过程是加锁的,加锁是通过可重入锁ReentrantLock实现的,如果不加锁的话,多个线程同时添加元素会复制多次。
    getArray()获得旧容器中的元素

    final Object[] getArray() {
            return array;
        }
    

    Arrays.copyOf(elements, len + 1)进行数组的复制,并返回复制以后新的数组
    newElements[len] = e向新的集合中添加元素
    setArray(newElements)将旧容器的引用指向新容器

     final void setArray(Object[] a) {
            array = a;
        }
    

    其余向容器中添加元素的方法,比如public void add(int index, E element)实现思路和add(E e)大抵相同
    2.get(int index),从容器中获得指定位置的元素

    public E get(int index) {
            return get(getArray(), index);
        }
    

    实际调用的是get(Object[] a, int index)方法

    private E get(Object[] a, int index) {
            return (E) a[index];
        }
    

    得到指定位置的元素就是获得数组中指定位置的元素
    从代码中可以看到,对于从容器中读操作是不进行加锁的
    3.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();
            }
        }
    

    实现的基本思想和add(E e)相同,需要进行加锁,并且会对旧的容器进行复制

    4.看一个CopyOnWriteArrayList的构造函数

    public CopyOnWriteArrayList() {
            setArray(new Object[0]);
        }
    

    一般我们自己写项目的时候都会选择使用这个构造函数。这个构造函数并没有初始化集合的大小,集合大小为0,但是它没有像ArrayList一样有扩容操作,因为在往这个容器中进行写操作时,实际上并不是往当前容器添加元素,而是会创建出一个比当前容器大1的容器,在对往这个容器中添加元素。所以不需要进行扩容。或者可以这么理解,它在每次添加元素的操作时都进行了一次扩容,每次扩容一个元素的大小

    5.CopyOnWriteArrayList的缺点:
    最明显的一个致命缺点就是占大量的内存,在往容器中删除元素和添加元素的时候都会在创建一个新的数组,如果垃圾收集器回收不及时的话,并且有很多线程进行写操作,可能会撑爆内存吧。
    在一篇博客上看到CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。不是特别理解这一点,volatile Object[] array,array是有volatile修饰的,其保证了内存的可见性,当一个线程对一个共享变量的写操作时,写完立刻就会对其他线程立即可见,那只要写完,其他线程就能读到新添加的值。自己写了个demo进行测试,感觉延迟效果不是很明显
    测试类:TestCopyOnWriteArrayList.java

    public class TestCopyOnWriteArrayList {
        private static CopyOnWriteArrayList<String> c = new CopyOnWriteArrayList<>();
        private static long startTime;
        private static long endTime;
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    c.add("a");
                    startTime = System.currentTimeMillis();  //获得添加a以后的时间
                    System.out.println("添加了a");
                    //添加了a以后让其睡眠,让其他线程有时间执行
                    try {
                        Thread.currentThread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    c.add("b");
                    System.out.println("添加了b");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.currentThread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    c.add("c");
                    System.out.println("添加了c");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.currentThread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    c.add("d");
                    System.out.println("添加了d");
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
    //                System.out.println("读取第一个元素");
                    String s = c.get(0);
                    endTime = System.currentTimeMillis();
                    System.out.println("读取到a花费时间:" + (endTime - startTime) + "毫秒");
                    System.out.println("s: " + s);
                }
            }).start();
        }
    }
    
    

    运行结果:

    添加了a
    读取到a花费时间:1毫秒
    s: a
    添加了b
    添加了c
    添加了d
    

    1毫秒的延迟也不是特别长吧
    不知道是不是自己的例子不正确
    6.CopyOnWriteArrayList的应用场景:读多写少的场景

    相关文章

      网友评论

        本文标题:CopyOnWriteArrayList

        本文链接:https://www.haomeiwen.com/subject/unhtnxtx.html