线程安全的list
在学list的时候,应该都了解过ArrayList是线程不安全的集合。今天学习线程安全的一个list集合。CopyOnWriteArrayList。
CopyOnWrite
从取名来看CopyOnWriteArrayList实质上就应该是个ArrayList。只是多了一个CopyOnWrite(写入时复制) 思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
好在了解了什么是写入时复制思想之后,再来看一下源码的具体实现:
/** The lock protecting all mutators */
transient final ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private volatile transient Object[] array;
首先,CopyOnWriteArrayList类中定义了一个ReentrantLock锁,我们知道ReentrantLock是一个可重入的独占锁。 还定义了一个volatile类型的 array。 ArrayList的底层就是基于数组实现。
CopyOnWriteArrayList也也是一样。数组被定义为volatile。是因为volatile定义的变量在多线程环境中可以保证其可见性。声明ReentrantLock就是用来保证线程的安全性。先看读取:
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
return get(getArray(), index);
}
对于读取而言,很简单,就是读取数组的某一个元素。并没有上锁,这就意味着可以多个线程同时读。
添加/修改/删除
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 void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} 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();
}
}
对于修改、删除、添加我们可以看到,都是先上独占锁,然后先把原来的数组复制一份,然后对复制出来的数组进行对应的操作,最后把新的数组赋值给volatile修饰的数组。因为数组是volatile修饰,这就保证了线程在修改之后,其它线程看到的一定是最新值。
缺点
- 在进行修改、删除、添加的时候,都需要对整个数组进行复制,会占用两份内存。如果对象占用的内存较大,就会引发频繁的垃圾回收行为,降低性能;
- CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。volatile修饰的只保证可见性,不保证原子性。
总结
CopyOnWrite容器来说,只适合在读操作远远多于写操作的场景下使用