锁性能提升——CopyOnWriteArrayList
2018-06-21 本文已影响0人
柚子过来
在说CopyOnWriteArrayList之前先说下ArrayList为什么不是线程安全的,通常意义上的线程安全是指在多个线程同时执行的 情况下不会出现数据不一致或者脏数据等,也就是多个线程一起执行与一个线程执行多次的结果应该是一样的。所以其实如果一个操作如果不是原子操作,又没有做线程安全的处理(如锁),那就会有线程安全问题。
比如我们看ArrayList的add方法:
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e; (1)
size = s + 1; (2)
}
从代码可以看出是先添加数据,再更新size,所以如果线程1执行到了(1),但是还没执行(2),这时线程2又add,那就会出现数据丢失。
与ArrayList对应的线程安全类有Vector,但是Vector是对add、get、remove等方法添加了synchronized同步保证的线程安全,所以性能较低。
在实际的应用场景中读操作可能远远多于写操作,所以去除get操作的同步开销可以提升性能,但是这时就要考虑到如果去除了get操作的同步,但是由于集合的fail-fast机制,如果去除了get操作的同步,如果一个线程在get,另一个线程add的话就会抛出ConcurrentModificationException异常。
CopyOnWriteArrayList使用了CopyOnWrite的思想,在add或者remove数据的时候,先复制一份数组的副本,对副本进行add操作,然后再将实际数组的引用指向副本,完成更新:
public boolean add(E e) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
}
}
final void setArray(Object[] a) {
array = a;
}
public E remove(int index) {
synchronized (lock) {
Object[] elements = getArray();
int len = elements.length;
E oldValue = elementAt(elements, index);
int numMoved = len - 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;
}
}
可以发现CopyOnWrite操作的复制行为保证了在进行写操作时,可以并发的进行读操作并且不会有并发问题。而写操作本身是加锁的,所以写操作也不会有并发问题
但是CopyOnWriteArrayList这样实现就有弱一致性的缺点,它只能保证最终一致性,因为线程读取到的可能不是最新的数据。