多线程并发 - CopyOnWrite 容器
一、CopyOnWrite 简介
Copy-On-Write 简称 COW,是计算机设计领域中的一种优化策略,也是一种在并发场景下常用的设计思想——写入时复制思想
JDK1.5
开始Java并发包里提供了两个使用CopyOnWrite
机制实现的并发容器,它们是CopyOnWriteArrayList
和CopyOnWriteArraySet
特点:
- 读取安全(不保证缓存一致性),写入安全(代价是加了锁,且需全量复制)
- 适用于对象空间占用大,修改次数少,而且对数据实效性要求不高的场景
- 不建议用于频繁读写场景下,全量复制很容易造成GC停顿,因此建议使用平时的Concurrent包来实现
CopyOnWrite
容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。
这样做的好处在于,我们可以在并发的场景下对容器进行"读操作"而不需要"加锁",从而达到读写分离的目的。
二、CopyOnWriteArrayList
1、介绍
CopyOnWriteArrayList
常被用于读多写少
的并发场景;因为CopyOnWriteArrayList
无需任何同步措施,大大增强了读的性能
当遍历 ArrayList
或 LinkedList
的时候,若中途有别的线程对List容器进行修改,则会抛出 ConcurrentModificationException
异常。而CopyOnWriteArrayList
由于其 读写分离
机制,遍历和修改操作分别作用在不同的 List
容器,所以在使用迭代器遍历的时候,则不会抛出异常。
缺点:
① 内存占用问题:CopyOnWriteArrayList
每次执行写操作都会将原容器进行拷贝一份,内存里会同时有两个对象,旧对象和新写入的对象;当数据量大的时候,内存会存在较大的压力,可能会造成频繁的 GC问题。
例如:假设此时对象占用100M,那再写入100M数据时,内存就会多占用200M。
② 数据一致性问题:由于实现的原因,写和读分别作用在不同新老容器上,在写操作执行过程中,读不会阻塞,但读取到的却是老容器的数据;CopyOnWrite
容器只能保证数据的最终一致性,不能保证数据的实时一致性
2、原理【源码分析】
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
private static final long serialVersionUID = 8673264195747942595L;
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
private static final Unsafe UNSAFE;
private static final long lockOffset;
//....省略其他方法
}
上述源码可见:CopyOnWriteArrayList
内部实际上就是维护了一个被 volatile
修饰的数组,来保证数据的内存可见性。
2.1、get()
方法 - 源码
final Object[] getArray() {
return this.array;
}
private E get(Object[] var1, int var2) {
return var1[var2];
}
public E get(int var1) {
return this.get(this.getArray(), var1);
}
上述源码可见,其获取元素的操作逻辑清晰:
get()
“读操作”是没有加锁,直接读取,效率是最高的
2.2、add()
方法 - 源码
public boolean add(E var1) {
// 使用lock,保证线程安全,写线程时同一时刻只有一个
ReentrantLock var2 = this.lock;
var2.lock();
boolean var6;
try {
//获取旧数据的组引用
Object[] var3 = this.getArray();
int var4 = var3.length;
//拷贝原容器,长度位原容器的长度+1
Object[] var5 = Arrays.copyOf(var3, var4 + 1);
//在新容器中添加新的数据
ar5[var4] = var1;
//将旧容器引用指向新的容器
this.setArray(var5);
var6 = true;
} finally {
//解锁
var2.unlock();
}
return var6;
}
上述源码可见,其添加元素的操作逻辑清晰:
- 采用
ReentrantLock
保证同一时刻只有一个写线程执行; - 创建新的容器(长度为:旧容器长度+1),把旧容器copy过来
- 在新容器进行写的操作,在进行新旧容器引用的切换
- 在 finally 块中释放锁,以便其他线程可以访问
2.3、remove()
方法 - 源码
public E remove(int var1) {
//加锁
ReentrantLock var2 = this.lock;
var2.lock();
Object var11;
try {
//获取容器内容
Object[] var3 = this.getArray();
int var4 = var3.length;
Object var5 = this.get(var3, var1);
int var6 = var4 - var1 - 1;
if (var6 == 0) {
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
this.setArray(Arrays.copyOf(var3, var4 - 1));
} else {
否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
Object[] var7 = new Object[var4 - 1];
System.arraycopy(var3, 0, var7, 0, var1);
System.arraycopy(var3, var1 + 1, var7, var1, var6);
this.setArray(var7);
}
var11 = var5;
} finally {
//释放锁
var2.unlock();
}
return var11;
}
上述源码可见,其删除元素的操作逻辑清晰:
- 采用
ReentrantLock
保证同一时刻只有一个删除线程执行 - 将 remove 元素之外的其他元素拷贝到新副本中,然后切换引用,再将原容器的引用指向新的副本
- 在 finally 块中释放锁,以便其他线程可以访问
三、CopyOnWriteArraySet
1、介绍
CopyOnWriteArraySet
是一个线程安全的集合容器,它使用了“写时复制”的技术来实现,并且具有可读性和数据不变性的特点。虽然它的写性能较差,但在读多写少的场景中,CopyOnWriteArraySet
能够提供更好的性能和可靠性。
其优缺点同 CopyOnWriteArrayList
一样
2、原理【源码分析】
CopyOnWriteArraySe
t 实现了 Set
接口,并且使用了 CopyOnWriteArrayList
来作为底层的数据结构,因此其内部方法的原理同CopyOnWriteArrayList
是一样的,可自行翻看源码查看,在此处则不进行分析了…