容器|青训营笔记

42 阅读2分钟

CopyOnWriteArrayList 支持两个原子方法: //不存在才添加,如果添加了,返回true,否则返回false,达到了去重效果 public boolean addIfAbsent(E e); //同上,返回实际添加的元素个数 public int addAllAbsend(Collection<? extends E> c); CopyOnWriteArrayList的实现原理很简单,内部使用ReentrantLock维护一个数组,读数据时直接通过下标返回数组元素,修改时通过ReentrantLock进行保护,然后通过原数组复制一个新数组,在新数组上进行修改,修改完后,再将老数组指向新数组。这样的好处是:如果在修改的时候,有别的线程访问,内容还是正确的,因为修改的是新数组,此时别的线程访问的是老数组。说白了,通过写时复制,将读取和修改分为两个目标,这是一种伟大的思想,不像synchronized和CAS对同一个目标进行保护,而是直接拆分为两个目标,巧妙的避开了资源竞争。我们来简单看下add()和get()源码: public boolean add(E e) { //加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //复制内容到新数组,新数组长度比老数组大1,因为要添加元素 Object[] newElements = Arrays.copyOf(elements, len + 1); //修改新数组 newElements[len] = e; //将新数组内容设置给老数组 setArray(newElements); return true; } finally { //解锁 lock.unlock(); } } 可以看到,add()是基于CAS的写时复制,高并发的时候也是安全的,接下来看get(): public E get(int index) { //返回数组对应下标的元素 return get(getArray(), index); } //返回当前数组 final Object[] getArray() { return array; } //直接根据下标返回数组元素 private E get(Object[] a, int index) { return (E) a[index]; } 可以看到,get()是不加锁的,因为它并没用修改数组,所以不存在安全问题,在一个for()循环内部,不断调用get()获取数据,可能每次得到的结果都不同,因为可能期间其他线程修改了数据,这也是完全合理的。 2 CopyOnWriteArraySet 这个实现比较简单,直接包装了CopyOnWriteArrayList实现,我们看下代码: //构造函数,直接创建一个 CopyOnWriteArrayList public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList(); } //add()方法直接调用内部CopyOnWriteArrayList的addIfAbsent(),从而达到去重效果 public boolean add(E e) { return al.addIfAbsent(e); } CopyOnWriteArrayList/Set使用了写时复制的思想,避免了资源竞争,但是复制本身效率较低,所以适用于读远大于写,读多写少,集合不大的场景。