CopyOnWrite
- 顾名思义,写时复制, 即做Write更新的操作时,进行复制。
- 那么为什么要这样子做呢? 接下来我们以CopyOnWriteArrayList的源码来做分析。
CopyOnWriteArrayList
CopyOnWriteArrayList
可以看成是ArrayList的线程安全版本,所以很多方面与ArrayList相同,我们略过一些相同的方面,先来看其主要属性
/** 可重入锁,用来保证写安全 */
transient final ReentrantLock lock = new ReentrantLock();
/** volatile数组.确保 set的时候修改引用地址的时候是原子操作 */
private volatile transient Object[] array;
- 两个属性,一个是数据, 一个是可重入锁来控制写。看一下构造函数。
/**
* 通过 set一个空Object数组进行初始化
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
/**
* 对于非Object类的, 转换为Object数组.再set
*/
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] 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);
}
/**
* 将传入的array复制为 Object数组
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
- 构造函数都比较简单, 就是set一个 Object数组,再看一下几个读操作
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
/**
*/
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
/**
* 通过遍历数组来确定位置.
*/
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
/**通过indexOf来确定是否包含.
*/
public boolean contains(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
- 读方法都很简单, 获取数组之后进行相关数组读。
- 再来看看几个比较典型的写方法, add、set、remove
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock(); //加锁. 防止同时有两个写线程进入.
try {
Object[] elements = getArray(); // 得到array的引用.
int len = elements.length; //判断 index是否合理.
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements; //定义新数组.
int numMoved = len - index; //需要移动的元素数量. 即.index之后的元素都需要移动一个位置.
if (numMoved == 0) //插到最后.
newElements = Arrays.copyOf(elements, len + 1); //copy原数组,并设置copy后的新数组长度为 原数组长度+1.
else {
newElements = new Object[len + 1]; //新数组比原数组多一个元素
System.arraycopy(elements, 0, newElements, 0, index); //copy前段.
System.arraycopy(elements, index, newElements, index + 1,
numMoved); //copy index之后的后段.
}
newElements[index] = element; //在目标位置设值.
setArray(newElements); //将 完成写操作之后的数组刷新到引用中.
} finally {
lock.unlock(); //写操作完毕解锁.
}
}
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;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
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;
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();
}
}
- 代码加上注释很清晰了, 需要注意的是:
- 这里用到了 lock,目的是阻止多线程写。同一时间,写操作只能有一个,这个是毋庸置疑的。
- 这里lock可以简单理解为, try到finally这段代码加了
synchronized
, 这样子就保证了多个线程调用这些写操作时安全。 - 另外一点, 则是这里用到了
Arrays.copyOf
和System.arraycopy
。 首先,Arrays.copyOf
本质上, 调用的还是System.arrayCopy的方法, 而System.arrayCopy
则是使用native代码(基于内存块的拷贝),从而使数组拷贝尽量快。 - 至于
add
方法, if 里使用Arrays.copy
而else使用System.arraycopy
而不使用Arrays.copyRange
。 我认为是因为在Arrays.copyRange
里加了一些范围之类的检查,而在这里是没有必要的,因此这里使用System.arraycopy
可以减少检查,效率更高.
- 另外这里还有一个我之前想不明白, 既然已经保证了写的安全,为什么要用copy, 而不在 getArray得到的数组里直接修改就好了。已经"安全"了,还复制有必要吗?
- 其实,这里如果不用
copy
, 只用了写lock, 只能保证写的安全,如果不用copy读是不安全的。 - 例如,迭代器正在迭代,然后就被删除了一个,那么会有问题。
- 另外,写元素的时候是非原子操作的,而读是没有
lock
的(但是lock了话就和Vector
或Collections.synchronizedList
没有太大区别了),如果不copy, 会造成同时读写有问题。 - 这就是为什么写已经安全了还要用copy的原因,是为了保证读也是安全的。
- 其实,这里如果不用
简单整理一下
在上面,我们发现, 写的时候不单有锁,并且还会进行copy, 可以发现:
1,写操作效率其实很低, 对于数据量比较大, 并且写操作比较频繁的场景是很不合适的。
2,读和写其实是分隔开了的, 除了写在同步时(setArray), 这两种操作不会互相影响。因此,写时复制在多线程频繁读的场景是比较合适的。在这种情景,该类的效率是大于Vector的, 读线程越多,表现越明显
另外,CopyOnWrite这里还有一个类, CopyOnWriteArraySet
不重复元素的集合, 其底层其实使用 CopyOnWriteArrayList
, 所以基本和CopyOnWriteArrayList
一样的, 可以自行浏览一下源码。