作为Java开发者都应该知道工作中最常用的ArrayList集合是属于线程不安全的。在它的底层源码实现中,没有限制线程的读写操作,因此在多线程的环境下,使用ArrayList是不够合理的。
想要线程安全,那么就使用Vector或者Collections工具类中的SynchronizedList吧。但是对于这两种集合来说,它的线程安全效果的底层实现是直接使用synchronize关键字修饰方法,也就是对整个集合上了锁,同一个时刻只能有一个线程方法访问集合。毫无疑问,效率太低,得不偿失。
针对于以上的问题,CopyOnWriteArrayList就此诞生了。
Copy-On-Write(COW设计思想)
为了更好理解之后的源码,需要先了解CopyOnWriteArrayList集合中的核心思想——COW。
我们使用集合最主要的操作就是“读写”,站在“读”的角度看,即是读操作是为了获取到集合中最新的数据,需要满足数据的实时性;而站在“写”的角度看,即是写操作是为了针对集合中的最新数据进行修改。
那么在多线程的情况下,如果不做任何限制,就会导致脏数据的产生,线程不安全。那么COW就是为了解决这种情况,它的核心思想就是通过“写时复制”来实现数据的一致性,并且能够保证“读线程间不阻塞”。
简单来说,当我们往集合中添加元素的时候,并不是直接往集合中添加元素,而是先将集合copy一份新集合出来,然后再往新集合中添加元素,添加完元素之后,再将新集合替换掉原集合。当需要从集合中获取元素的时候,不需要做任何限制,因为此时的集合不会执行添加元素的操作,但是此时由于“写时复制”的原因,可能获取到的元素并不是最新的数据。
所以COW是一种读写分离的思想,通过在写操作的时候针对的是不同的集合来实现的,同时也牺牲了数据实时性的来追求数据一致性。
CopyOnWriteArrayList的重要属性
// 独占锁(保证每次只能有一个写操作访问集合)
final transient ReentrantLock lock = new ReentrantLock();
// 内部维护一个数组实现存取数据
private transient volatile Object[] array;
final Object[] getArray() { // 获取数组
return array;
}
final void setArray(Object[] a) { // 重新赋值数组
array = a;
}
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); // 复制一个新数组,长度为原数组的长度 + 1
newElements[len] = e; // 将数据放入新数组最后一个空位
setArray(newElements); // 将新数组替换掉原数组
return true;
} finally {
lock.unlock(); // 释放锁
}
}
可以看到集合add添加元素的方法中有独占锁的存在,说明在每次都只有一个添加的操作能够访问集合。这样保证了写操作时候数据的一致性和实时性,但是性能方面就有所下降。
get
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
这个get方法就太简单了,没什么好说的。这里主要是说明,get读操作不受任何限制,读写之间没有联系,所谓读写分离。
remove
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock(); // 获取锁
try {
Object[] elements = getArray();
int len = elements.length;
E oldValue = get(elements, index);
int numMoved = len - index - 1; // 或需要移动的索引位置numMoved(因为删除一个元素后,需要将后面的元素往前移动一位)
// 将index位置后面的元素往前移动一位,补齐空位,同时数组长度减1
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
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 {
lock.unlock(); // 释放锁
}
}
总结
到此,对于CopyOnWriteArrayList的源码解析结束了。其实通过这篇博文可以知道,CopyOnWriteArrayList的底层非常简单,无非就是独占锁和写时复制的结合,从而实现了读写分离的效果。但从底层实现上看,其实CopyOnWriteArrayList更适合用于读操作大于写操作的场景。
但CopyOnWriteArrayList虽然有读写分离的优点,同时也有它的缺点:
-
占用内存:因为在写操作时候,会将原数组复制一份,然后最后将新数组替换掉旧数组。而这里的替换,仅仅只是指针的指向改变了,原数组的仍然存在于内存当中,等待GC的回收。那么在GC回收的之前这段时间里,如果有大量的写操作涌入,而且此时数组的数据量也很大了,那么这个时间的内存将会存在大量的被弃用的数组,白白占用内存空间。
-
数据实时性问题:在本文一开始就讲过了,CopyOnWriteArrayList牺牲了数据实时性,实现了数据一致性。每次读取的数据很可能并不是最新的数据,因此对数据实时性有要求的话,不要使用CopyOnWriteArrayList。