java容器——CopyOnWriteArrayList/Set
夯实基础系列:
基础是否扎实,决定你是否能走的更稳更远。
Copy-On-Write
Copy-On-Write(COW),写时复制,是一种程序优化策略:即所有人共享同一个对象,当某个线程修改时,需要将内容copy一份进行修改。修改后,所有线程访问修改后的对象。
这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器:CopyOnWriteArrayList和CopyOnWriteArraySet。
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
主要方法
访问类方法——无锁访问
访问类方法,主要包括get(), indexof(), contains(), empty(), size()等方法
/**
* The lock protecting all mutators. (We have a mild preference
* for builtin monitors over ReentrantLock when either will do.)
*/
final transient Object lock = new Object();
/** The array, accessed only via getArray/setArray. */
// Android-changed: renamed array -> elements for backwards compatibility
private transient volatile Object[] elements;
final Object[] getArray() {
return elements;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* @throws IndexOutOfBoundsException
*/
public E get(int index) {
return get(getArray(), index);
}
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; //允许有null的
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
(TODO,这里留个小坑,java对象相等的判断)
修改类方法——加锁、复制
修改类的方法包括:set(), add(), remove(), removeRange(), addIfAbsent()等方法。
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);
}
return oldValue;
} finally {
lock.unlock();
//这里加锁用到了lock,为什么不用Sychronized呢?当然为了优化性能了
}
}
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
* @throws IndexOutOfBoundsException
*/
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();
}
}
存在的问题
内存占用问题
copy-on-write采用写时复制的策略,所以在写操作的时候,内存中会同时存在两个对象,当内存占用高的时候,会导致yong gc或full gc。原来对象占用内存200M,新写入100M。内存中的对象300M?500M?
同时这也提醒在使用copy-on-write机制时,注意不应保存过大的对象。
数据一致性问题
copy-on-write只能保证数据的最终一致性,不能保证数据的实时一致性。
CopyOnWrite的应用场景
CopyOnWrite并发容器用于读多写少的并发场景,比如白名单黑名单之类。
课后思考题:
1、还记得对源容器加锁是怎么实现的吗?为什么不用Sychronized?