CopyOnWriteArrayList

今天来复习一下集合。在支持并发的集合中,我觉得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 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)