CopyOnWriteArrayList详解
简介
Copy-On-Write(COW):
开始时共享同一资源,但一个线程修改部分内容时,才会用修改后的内容覆盖旧的内容.是一种延时懒惰策略.
有两种COW机制的并发容器:
CopyOnWriteArrayList
.CopyOnWriteArraySet
.
原理
- 是一种写时复制容器.
- 当向容器添加元素时,不直接添加元素,而是先将当前容器复制一份,向复制得到的新容器添加元素,再将原容器的引用指向新的容器.
- 并发读时不需要加锁,并发写时加锁.
- 读写分离,读和写为不同的容器.
实现
添加元素
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); // 复制原容器中的元素
newElements[len] = e; // 添加新增的元素
setArray(newElements); // 将引用指向新容器
return true;
} finally {
lock.unlock(); // 释放锁
}
}
步骤:
- 加锁
- 复制原来的元素到新容器中
- 向新容器中添加元素
- 更新引用指向新容器
获取元素
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 { // 若指定位置的元素与设定的元素相同,则无需更新
setArray(elements);
}
return oldValue;
} finally {
lock.unlock(); // 释放锁
}
}
步骤:
- 获取旧的容器.
- 若指定位置的元素与设定的新值不同,则构造新容器,并更新该位置上的值.
- 若设定的新值与旧值相同,则无需更新.
删除元素
类似与添加元素.
- 加锁
- 构造新容器
- 更新引用指向新容器
- 释放锁
添加不存在的元素
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加锁
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1); // 复制旧容器中的元素
newElements[len] = e; // 向新容器中新增元素
setArray(newElements); // 更新引用
return true;
} finally {
lock.unlock(); // 解锁
}
}
流程:
- 加锁
- 检查新增的元素是否已存在,若已存在,则无需新增
- 若不存在,则构造新容器,将旧值复制到新容器中,并添加新值
- 释放锁
应用
- 用于读多写少的并发场合.(白名单,黑名单,商品类目的访问等)
- 注意:
- 减少扩容,初始化大小,减少扩容的开销.
- 使用批量添加.每次添加都会进行复制,减少复制的次数.
- 问题:
- 内存占用高.进行写操作时,内存中同时存在旧数据和新数据,可能导致频繁的Young GC和Full GC.可通过压缩容器中的元素大小实现.
- 数据一致性.CopyOnWriteArrayList只保证数据的最终一致性,不保证数据的实时一致性.若希望写入的数据实时读到,不可使用CopyOnWrite容器.
参考: