并发容器:CopyOnWriteArrayList

0.ConpyOnWriteArrayList的使用场景

在多线程中,若是A线程在读取一个List时,B线程在向里面add数据,会抛出异常:java.util.ConcurrentModificationExceptionjava

public class ErrorTest {

    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

        final ArrayList<Integer> list_thrd = new ArrayList<>(list);
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    list_thrd.add(count++);
                }
            }
        });

        thread.setDaemon(true);
        thread.start();

        Thread.sleep(3);
        for (Integer integer : list_thrd) {
            System.out.println(integer);
        }
    }

}

运行这段代码,会抛出java.util.ConcurrentModificationException,即主线程在遍历list元素的时候,子线程在向里面添加元素。在这种状况下,咱们可使用CopyOnWriteArrayList进行操做。多线程

1.初步了解CopyOnWriteArrayList

CopyOnWrite,字面意思就是写时复制。通俗的说,就是咱们向容器中添加数据的时候,并非向容器自己添加数据。而是,先copy一个副本,向里面添加记录,而后再将copy赋值给原对象。咱们来看下面的代码并发

public class CopyOnWriteListTest {

    public static void main(String[] args) throws InterruptedException {
        List<Integer> temp = Arrays.asList(1, 2, 3, 4, 5);

        final CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(temp);

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    list.add(count++);
                }
            }
        });

        thread.setDaemon(true);
        thread.start();

        Thread.sleep(3);
        for (Integer integer : list) {
            System.out.println(list.hashCode());
            System.out.println(integer);
        }
    }

}

咱们在遍历CopyOnWriteArrayList集合的时候,同时打印出hashCode,能够看到输出结果:ide

-1813306013
1
1381289098
2
1109838168
3
-1094901371
4
-1008565321
5
-1813169375
0
-1540165649
1
-1441641759
2
1807730922
3
-870434537
4
-1185576945
5

这说明CopyOnWriteArrayList的add方法确实是建立了新的list对象,同时也不会抛出异常。this

2.CopyOnWriteArrayList的实现原理

2.1 add方法

/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
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();
        }
    }
  • 添加方法使用了重入锁来保证多线程添加的时候,不会生成多个list的副本在内存中;
  • array对象使用了volatile修饰,保证了多线程环境下的可见性
  • 最后使用setArray来给array对象赋值

2.2 get方法

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

get方法没有使用锁,因此读取的仍是旧的数据,由于写入的时候并无锁住旧的array对象spa

3.CopyOnWriteArrayList的使用场景 

从刚刚的例子中咱们能够看出,CopyOnWriteArrayList适合读多写少的场景。例如黑白名单等,将数据初始化到List当中,当有较少写操做的时候,能够保证多并发状况下,List的最终一致性。可是,它也是有缺点的:当执行add操做的时候,因为要copy出一个对象,这时会致使内存占用加大。可能会形成频繁的Young GC和Full GC。线程

相关文章
相关标签/搜索