这篇文章的目的如下:
- 了解一下ArrayList和CopyOnWriteArrayList的增删改查实现原理
- 看看为什么说ArrayList查询快而增删慢?
- CopyOnWriteArrayList为什么并发安全且性能比Vector好
1. List接口
首先我们来看看List接口,因为ArrayList和CopyOnWriteArrayList都是实现了List接口,我们今天主要是研究增删改查原理,所以只看相应的方法即可。
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex); }
2 ArrayList
2.1 几个重点
- 底层是数组,初始大小为10
- 插入时会判断数组容量是否足够,不够的话会进行扩容
- 所谓扩容就是新建一个新的数组,然后将老的数据里面的元素复制到新的数组里面
- 移除元素的时候也涉及到数组中元素的移动,删除指定index位置的元素,然后将index+1至数组最后一个元素往前移动一个格
1 1)增 2 public boolean add(E e) { 3 //进行数组容量判断,不够就扩容 4 ensureCapacityInternal(size + 1); // Increments modCount!! 5 elementData[size++] = e; 6 return true; 7 } 8 9 10 public void add(int index, E element) { 11 //检查是否会越界 12 rangeCheckForAdd(index); 13 //进行数组容量判断,不够就扩容 14 ensureCapacityInternal(size + 1); // Increments modCount!! 15 //将index至数据最后一个元素整体往后移动一格,然后插入新的元素 16 System.arraycopy(elementData, index, elementData, index + 1, 17 size - index); 18 elementData[index] = element; 19 size++; 20 } 21 2) 删 22 public E remove(int index) { 23 //判断是否越界 24 rangeCheck(index); 25 26 modCount++; 27 E oldValue = elementData(index); 28 29 int numMoved = size - index - 1; 30 //若该元素不是最后一个元素的话,将index+1至数组最后一个元素整体向前移动一格 31 if (numMoved > 0) 32 System.arraycopy(elementData, index+1, elementData, index, 33 numMoved); 34 elementData[--size] = null; // clear to let GC do its work 35 36 return oldValue; 37 } 38 39 3)改 40 public E set(int index, E element) { 41 rangeCheck(index); 42 43 E oldValue = elementData(index); 44 elementData[index] = element; 45 return oldValue; 46 } 47 48 4) 插 49 public E get(int index) { 50 rangeCheck(index); 51 52 return elementData(index); 53 } 54 55 E elementData(int index) {return (E) elementData[index]; }
2.2 总结
-
ArrayList的底层是数组,所以查询的时候直接根据索引可以很快找到对应的元素,改也是如此,找到index对应元素进行替换。而增加和删除就涉及到数组元素的移动,所以会比较慢
3 CopyOnWriteArrayList
3.1 几个要点
-
实现了List接口
-
内部持有一个ReentrantLock lock = new ReentrantLock();
-
底层是用volatile transient声明的数组 array
-
读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
1 1)增 2 3 public boolean add(E e) { 4 final ReentrantLock lock = this.lock; 5 //获得锁 6 lock.lock(); 7 try { 8 Object[] elements = getArray(); 9 int len = elements.length; 10 //复制一个新的数组 11 Object[] newElements = Arrays.copyOf(elements, len + 1); 12 //插入新值 13 newElements[len] = e; 14 //将新的数组指向原来的引用 15 setArray(newElements); 16 return true; 17 } finally { 18 //释放锁 19 lock.unlock(); 20 } 21 } 22 23 24 public void add(int index, E element) { 25 final ReentrantLock lock = this.lock; 26 lock.lock(); 27 try { 28 Object[] elements = getArray(); 29 int len = elements.length; 30 if (index > len || index < 0) 31 throw new IndexOutOfBoundsException("Index: "+index+ 32 ", Size: "+len); 33 Object[] newElements; 34 int numMoved = len - index; 35 if (numMoved == 0) 36 newElements = Arrays.copyOf(elements, len + 1); 37 else { 38 newElements = new Object[len + 1]; 39 System.arraycopy(elements, 0, newElements, 0, index); 40 System.arraycopy(elements, index, newElements, index + 1, 41 numMoved); 42 } 43 newElements[index] = element; 44 setArray(newElements); 45 } finally { 46 lock.unlock(); 47 } 48 } 49 2)删 50 51 public E remove(int index) { 52 final ReentrantLock lock = this.lock; 53 //获得锁 54 lock.lock(); 55 try { 56 Object[] elements = getArray(); 57 int len = elements.length; 58 E oldValue = get(elements, index); 59 int numMoved = len - index - 1; 60 if (numMoved == 0) 61 //如果删除的元素是最后一个,直接复制该元素前的所有元素到新的数组 62 setArray(Arrays.copyOf(elements, len - 1)); 63 else { 64 //创建新的数组 65 Object[] newElements = new Object[len - 1]; 66 //将index+1至最后一个元素向前移动一格 67 System.arraycopy(elements, 0, newElements, 0, index); 68 System.arraycopy(elements, index + 1, newElements, index, 69 numMoved); 70 setArray(newElements); 71 } 72 return oldValue; 73 } finally { 74 lock.unlock(); 75 } 76 } 77 3)改 78 79 public E set(int index, E element) { 80 final ReentrantLock lock = this.lock; 81 //获得锁 82 lock.lock(); 83 try { 84 Object[] elements = getArray(); 85 E oldValue = get(elements, index); 86 87 if (oldValue != element) { 88 int len = elements.length; 89 //创建新数组 90 Object[] newElements = Arrays.copyOf(elements, len); 91 //替换元素 92 newElements[index] = element; 93 //将新数组指向原来的引用 94 setArray(newElements); 95 } else { 96 // Not quite a no-op; ensures volatile write semantics 97 setArray(elements); 98 } 99 return oldValue; 100 } finally { 101 //释放锁 102 lock.unlock(); 103 } 104 } 105 4)查 106 107 //直接获取index对应的元素 108 public E get(int index) {return get(getArray(), index);} 109 private E get(Object[] a, int index) {return (E) a[index];}
3.2 总结
从以上的增删改查中我们可以发现,增删改都需要获得锁,并且锁只有一把,而读操作不需要获得锁,支持并发。为什么增删改中都需要创建一个新的数组,操作完成之后再赋给原来的引用?这是为了保证get的时候都能获取到元素,如果在增删改过程直接修改原来的数组,可能会造成执行读操作获取不到数据。
4 CopyOnWriteArrayList为什么并发安全且性能比Vector好
我知道Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。