文章目录
- 什么是CopyOnWrite容器
- CopyOnWriteArrayList
- 优点
- 缺点
- 源码示例
- 仿写:CopyOnWriteMap的实现
- 注意事项
什么是CopyOnWrite容器
CopyOnWrite容器是一种实现了写时复制(Copy-On-Write,COW)机制的并发容器。在并发场景中,多个线程可能同时访问同一资源,当某个线程需要修改数据时,系统会创建该数据的副本供其修改,而其他线程仍然可以访问原始数据。这种机制的主要优点是可以在读操作频繁的情况下,避免加锁,从而提高读取性能。
在Java中,从JDK 1.5开始,提供了两个主要的CopyOnWrite容器:CopyOnWriteArrayList
和CopyOnWriteArraySet
。这两个容器的设计使得在“读多写少”的场景下,能够有效地提高并发性能。
CopyOnWriteArrayList
优点
-
无需同步:
CopyOnWriteArrayList
在进行读取操作时不需要任何同步措施,极大地提高了读取性能。 -
避免异常:在遍历时,即使有其他线程对列表进行修改,也不会抛出
ConcurrentModificationException
异常,因为读取和写入操作分别作用于不同的容器。
缺点
-
内存开销:每次写操作都会复制整个容器,导致内存使用增加,可能引发频繁的垃圾回收(GC)问题。
-
数据一致性问题:由于写操作和读操作作用于不同的容器,读操作可能读取到旧数据,因此不能保证实时一致性。
源码示例
CopyOnWriteArrayList
的add
和remove
方法的实现逻辑如下:
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效率最高的读操作的源码
public E get(int index) {return get(getArray(), index);
}
private E get(Object[] a, int index) {return (E) a[index];}
由上可见“读操作”是没有加锁,直接读取。
仿写:CopyOnWriteMap的实现
虽然Java并发包中没有提供CopyOnWriteMap
,但可以参考CopyOnWriteArrayList
实现一个简单的版本。以下是一个基本的实现示例:
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
容器只能保证最终一致性,不能保证实时一致性,因此不适用于需要立即读取写入数据的场景。