文章目录
什么是CopyOnWrite容器
CopyOnWrite容器是一种实现了写时复制(Copy-On-Write,COW)机制的并发容器。在并发场景中,多个线程可能同时访问同一资源,当某个线程需要修改数据时,系统会创建该数据的副本供其修改,而其他线程仍然可以访问原始数据。这种机制的主要优点是可以在读操作频繁的情况下,避免加锁,从而提高读取性能。
在Java中,从JDK 1.5开始,提供了两个主要的CopyOnWrite容器:CopyOnWriteArrayList
和CopyOnWriteArraySet
。这两个容器的设计使得在"读多写少"的场景下,能够有效地提高并发性能。
CopyOnWriteArrayList
优点
-
无需同步 :
CopyOnWriteArrayList
在进行读取操作时不需要任何同步措施,极大地提高了读取性能。 -
避免异常 :在遍历时,即使有其他线程对列表进行修改,也不会抛出
ConcurrentModificationException
异常,因为读取和写入操作分别作用于不同的容器。
缺点
-
内存开销:每次写操作都会复制整个容器,导致内存使用增加,可能引发频繁的垃圾回收(GC)问题。
-
数据一致性问题:由于写操作和读操作作用于不同的容器,读操作可能读取到旧数据,因此不能保证实时一致性。
源码示例
CopyOnWriteArrayList
的add
和remove
方法的实现逻辑如下:
java
public boolean add(E e) {
// ReentrantLock加锁,保证线程安全
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 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)
// 如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
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();
}
}
再来看看CopyOnWriteArrayList效率最高的读操作的源码
java
public E get(int index) {
return get(getArray(), index);
}
java
private E get(Object[] a, int index) {
return (E) a[index];
}
由上可见"读操作"是没有加锁,直接读取。
仿写:CopyOnWriteMap的实现
虽然Java并发包中没有提供CopyOnWriteMap
,但可以参考CopyOnWriteArrayList
实现一个简单的版本。以下是一个基本的实现示例:
java
import java.util.HashMap;
import java.util.Map;
public class CopyOnWriteMap<K, V> implements Map<K, V> {
private volatile Map<K, V> internalMap = new HashMap<>();
public V put(K key, V value) {
synchronized (this) {
Map<K, V> newMap = new HashMap<>(internalMap);
V oldValue = newMap.put(key, value);
internalMap = newMap;
return oldValue;
}
}
public V get(Object key) {
return internalMap.get(key);
}
public void putAll(Map<? extends K, ? extends V> m) {
synchronized (this) {
Map<K, V> newMap = new HashMap<>(internalMap);
newMap.putAll(m);
internalMap = newMap;
}
}
}
CopyOnWriteMap
适用于"读多写少"的场景,例如一个搜索网站的黑名单管理系统。黑名单在特定时间更新,而在用户搜索时,系统需要快速检查关键字是否在黑名单中。
注意事项
使用CopyOnWrite
容器时需要注意:
-
内存开销:应根据实际需求初始化容器的大小,以减少扩容带来的开销。
-
数据一致性 :
CopyOnWrite
容器只能保证最终一致性,不能保证实时一致性,因此不适用于需要立即读取写入数据的场景。