Java中的拷贝数组CopyOnWriteArrayList详解
作者:Brain_L
CopyOnWriteArrayList详解
ArrayList和LinkedList都不是线程安全的,如果需要线程安全的List,可以使用Collections.synchronizedList来生成一个同步list,但是这个同步list的方法都是通过synchronized修饰来保证同步的,并发性能不高。那么如何提高并发性能呢?比如某些场景下,对List的读操作远多于写操作,那么CopyOnWriteArrayList就派上用场了。
1、属性
/** The lock protecting all mutators */ final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ private transient volatile Object[] array;
lock是用来在写操作时加锁使用的,具体使用下面方法部分再看。
数组array是CopyOnWriteArrayList的核心部分了,所有的元素都存放在这个数组中。为了保证可见性,所以被volatile修饰着。
2、方法
final Object[] getArray() { return array; } final void setArray(Object[] a) { array = a; } public CopyOnWriteArrayList() { setArray(new Object[0]); } public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } setArray(elements); } public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
三个构造函数都是为了给array数组赋值,生成初始数组。
下面看下它的几个关键方法:get、add、remove
private E get(Object[] a, int index) { return (E) a[index]; } /** * {@inheritDoc} * * @throws IndexOutOfBoundsException {@inheritDoc} */ public E get(int index) { return get(getArray(), index); }
get比较简单,就是直接取数组索引处的元素。注意get方法没有加锁。
public boolean add(E e) { final ReentrantLock lock = this.lock; //1、加锁 lock.lock(); try { //2、取原数组 Object[] elements = getArray(); int len = elements.length; //3、拷贝生成新数组 Object[] newElements = Arrays.copyOf(elements, len + 1); //4、新元素加到数组最后一位 newElements[len] = e; //5、数组替换 setArray(newElements); return true; } finally { //6、释放锁 lock.unlock(); } }
add方法,上来就先加锁,然后取出原数组后拷贝生成了一个新的数组,注意,此时原有的array数组没有变,get访问时还是跟之前一样。当把新的数组替换掉array后,由于是volatile修饰的,get访问时就会访问添加过元素的新数组。这样就保证了读写同时进行时,读不需要加锁依然不会有并发问题。最后释放锁后,别的写操作获得锁,再次进行替换操作,这样保证写操作与写操作之间不会有并发问题。
public E remove(int index) { final ReentrantLock lock = this.lock; //1、获取锁 lock.lock(); try { //2、取原数组 Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); int numMoved = len - index - 1; //3、如果被删除元素是数组最后一位,直接截取len-1的新数组 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); //4、否则,分段拷贝生成新数组 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 { //5、释放锁 lock.unlock(); } }
remove与add同理,也是先获取锁,同时生成新的数组之后再把原有的数组进行替换。
3、总结
- 所有的元素均存储到数组中
- 写操作(add/set/remove等)会改变数组结构,采用新生成一个数组然后进行替换的做法,保证了在此期间原数组可以正常访问。同时操作之前需要先获取锁,避免写操作之间产生并发问题。
- 读操作由于不需要改变数组结构,且写操作时,对原有的数组不进行修改,此时仍可正常读取。写操作将新数组进行替换后,由于数组被volatile修饰,保证了可见性,此时也可正正常读取。所以读操作不需要加锁。
- 因为每次写操作都会带来数组拷贝,所以当读操作远大于写操作时,才可考虑使用此容器。
到此这篇关于Java中的拷贝数组CopyOnWriteArrayList详解的文章就介绍到这了,更多相关CopyOnWriteArrayList详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!