当前位置: 代码迷 >> 综合 >> JavaSE(14)——CopyOnWriteArrayList
  详细解决方案

JavaSE(14)——CopyOnWriteArrayList

热度:85   发布时间:2024-02-08 09:29:43.0

CopyOnWriteArrayList解析

1. Vector和SynchronizedList

1.1 回顾

? 众所周知,ArrayList是用于替代Vector的,Vector是线程安全的容器,因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全。

? 如果使用Collections.synchronizedList(new ArrayList())来使得ArrayList线程安全的话,也是几乎在每个方法上都加上了synchronized关键字,不同的是,它是加在方法内部,而不是方法声明上。

1.2 可能存在的问题

? 不管是使用Vector还是使用SynchronizedList,在多个线程中对该对象进行操作时,都会发生线程安全问题。唯一的解决办法就是在线程中操作对象之前,给该集合对象加锁,但是这样每次进行操作时都需要加锁,程序的效率会大打折扣。

? 因此,就需要使用CopyOnWriteArrayList

2. CopyOnWriteArrayListSet介绍

? 一般来说,我们都认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArrayListSet是同步Set的替代品。

? 无论是HashTable==》ConcurrentHashMap,还是Vector==》CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的区别。

  • HashTable、Vector加锁的粒度大,直接在方法声明处使用synchronized。
  • ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小 (用各种方式来实现线程安全,比如ConcurrentHashMap使用了cas锁、volatile等方法实现线程安全)
  • JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常

? 所以一般来说,我们都会使用JUC包下提供的线程安全容器,而不是老一代的线程安全容器。

3. CopyOnWriteArrayList实现原理

如果有多个调用者 (callers) 同时请求相同资源 (如内存或磁盘上的数据存储) ,他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用的副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。

优点是如果调用者没有修改该资源,就不会有副本被建立,因此多个调用者只是读取操作时可以共享同一份资源

  • CopyOnWriteArrayList是线程安全容器 (相对于ArrayList) ,底层通过复制数组的方法来实现。
  • CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
  • 元素可以为null

4. CopyOnWriteArrayList源码

4.1 属性和构造方法

public class CopyOnWriteArrayList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {//重入锁final transient ReentrantLock lock = new ReentrantLock();//存储数组,只能通过getArray/setArray访问private transient volatile Object[] array;//获取数组final Object[] getArray() {return array;}//设置数组final void setArray(Object[] a) {array = a;}//初始化数组 设置一个空数组public CopyOnWriteArrayList() {setArray(new Object[0]);}
}

4.2 常见方法

根据线程安全分析,如果遍历Vector/SynchronizedList是需要自己手动加锁的。

CopyOnWriteArrayList使用迭代器遍历时不需要显式加锁。

  • 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();}
    }
    
  • size方法

    public int size() {return getArray().length;
    }
    
  • get方法

    public E get(int index) {return get(getArray(), index);
    }private E get(Object[] a, int index) {return (E) a[index];
    }
    
  • set方法

    public E set(int index, E element) {//加锁final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();E oldValue = get(elements, index);//原元素与新元素不同,则覆盖原来的元素if (oldValue != element) {int len = elements.length;//操作的是副本数组,此时get方法不受影响Object[] newElements = Arrays.copyOf(elements, len);newElements[index] = element;setArray(newElements);} else {//元素相同,无操作setArray(elements);}return oldValue;} finally {//完成操作后解锁lock.unlock();} 
    }
    
  • 对于remove,clear方法跟set,add类似

总结

  • 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
  • 写加锁,读不加锁

4.3 迭代方法

public Iterator<E> iterator() {return new COWIterator<E>(getArray(), 0);
}//迭代器成员属性
//被迭代数组
private final Object[] snapshot;
//被返回元素的下标
private int cursor;private COWIterator(Object[] elements, int initialCursor) {cursor = initialCursor;snapshot = elements;
}

迭代器中的方法都是基于snapshot数组进行的,而snapshot是被传入的原数组,因此,CopyOnWriteArrayList在用迭代器遍历时,操作的都是原数组

在多线程情况下,其他线程执行clear等加锁操作时,会创建一个副本数组,进行操作,不影响原数组

4.4 缺点

  • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据的话,经常要执行add,set,remove方法,这是比较耗费内存的。
    • 这几个方法都是要复制一个数组出来的
  • 数据一致性:CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性
    • 当线程A在迭代CopyOnWriteArrayList容器中的数据,此时线程B在迭代中进行了增删改操作,调用了setArray方法,数据已经修改了,但是线程A迭代出来的还是原有的数据。

5. CopyOnWritSet

? CopyOnWriteSet的底层就是CopyOnWriteArrayList

private final CopyOnWriteArrayList<E> al;public CopyOnWriteArraySet() {al = new CopyOnWriteArrayList<E>();
}