CopyOnWrite
WIKI
Copy-on-write (CoW or COW), sometimes referred to as implicit sharing[[1]](https://en.wikipedia.org/wiki/Copy-on-write#cite_note-1) or shadowing,[2] is a resource-management technique used in computer programming to efficiently implement a "duplicate" or "copy" operation on modifiable resources.[3] If a resource is duplicated but not modified, it is not necessary to create a new resource; the resource can be shared between the copy and the original. Modifications must still create a copy, hence the technique: the copy operation is deferred to the first write. By sharing resources in this way, it is possible to significantly reduce the resource consumption of unmodified copies, while adding a small overhead to resource-modifying operations.
简单翻译:
COW有时候是指implicit sharing隐式共享, QT中的一个概念
和shadowing. 它是一种资源管理技术.它通过编程的方式针对可编辑的资源此处强调可编辑的资源对我的提示是: 关于高并发无外乎两种思路, 一种是资源共享(java并发模型), 另外一种是消息传递(actor模型, go语言的channel), 在java并发模型中, 共享资源在可编辑时存在线程安全的问题, 这也正是COW在java中的应用场景
如果没有编辑, 就没有必要创建一个新的资源, 这个资源仍然可以共享. 如果需要修改资源就必须先copy, 这样可以显著的减少资源的消耗.
COW应用场景
-
linux fork()
传统的fork()系统调用直接把所有的资源复制给新创建的进程。这种实现过于简单并且效率低下,因为它拷贝的数据也许并不共享,更糟的情况是,如果新进程打算立即执行一个新的映像,那么所有的拷贝都将前功尽弃。Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。
-
QT implicit sharing
很多Qt中的C++类运用了隐式数据共享机制去最大化资源有效利用和最小化复制克隆操作。隐式共享类当作为函数参数传递的时候,不仅安全而且效率很高,因为传递的时候只是传递的数据的指针,数据本身只当自己被修改的时候才会去复制。简称写时复制。
-
Java CopyOnWrite容器
COW的本意是copy操作的懒加载, 提高资源的使用效率. 看似和java并发没有关系, 但是万能的
Doug Lea
大师将这两者联系了起来. 例如要实现java List的线程安全, 可以简单的对读写操作加锁, 并不需要所谓的副本. 那么为什么要copy呢, 答案是进行读写分离, 大幅度提高读的性能. 写之前建立copy, 读的时候读origin, 写完之后将origin的指针指向copy. 读写相互不干扰.
CopyOnWriteArrayList
CopyOnWriteArrayList 源码摘要
- 先来看写, 在add的时候通过ReentrantLock进行加锁. 可以看到每次add操作都是同步的, CopyOnWriteArrayList并不会提高add的性能, 并且从代码看, add的时候增加了对数组的操作反而会影响性能.
- getArray() Arrays.copyOf setArray(newElements) 是关键操作. getArray()是获取origin, Arrays.copyOf是获取origin的副本, setArray(newElements)是修改origin的指向.
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();
}
}
-
addIfAbsent, 如果容器中没有则添加.
在添加之前先通过indexOf判断容器中是否已经存在了该元素. 如果没有则进行步骤二.需要注意的是indexOf方法是不加lock的, 这样做的目的是提高性能. 如果容器中已经存在的话就直接返回false. 但是因为这一步是没有加锁的, 所以snapshot和current并不能保证一致, 所以步骤二需要加锁进行进一步的判断.
优化1标记处是一个优化, 优化的策略是分段查找current. 如果current比snapshot长的话, current前半段的对比可以用 != 短路 equals, 从而提高对比效率.
感触:
这点效率都扣... 自己平时写的代码和这个比优化的空间得有多少啊
// 步骤一
public boolean addIfAbsent(E e) {
Object[] snapshot = getArray();
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
// 步骤二
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) {
// Optimize for lost race to another addXXX operation
// 优化1
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
// 如果current[i] 和snapshot[i]相等则一定不是e, 进行短路
// 这么优化的原因推测是 != 比 equals 效率高
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
// 判断current后半段不存在e
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();
}
}
CopyOnWriteArrayList优缺点和应用场景
-
写加锁并且有副本操作, 性能不高, 不适用于高并发写的场景
-
https://www.cnblogs.com/dolphin0520/p/3938914.html
读博客发现有内存问题, 我感觉是谬误, array中存的并不是对象, 而是对象的地址. 在array copy的时候拷贝的实际上是原对象的地址.#6楼 2018-01-01 14:29 甜甜咿呀咿呀哟 @ 岁月下的车辙 因为旧的数组可能没有及时被垃圾收集器回收掉
这点也不成立. origin数组和copy数组确实会同时存在一短时间, 但是由于add操作的时间不会很长, 再加上数组存的是对象地址, 所以并不会有太多的内存浪费. 退一步讲, 如果写并发超高, 同时有大量array副本, 确实会造成内存浪费, 但是也不太可能造成full gc, add操作完成后, minor gc就可以把失效的array副本gc掉了.
#7楼 2018-01-01 14:31 甜甜咿呀咿呀哟 volatile Object[] array,有volatile修饰不是立刻能被其他线程知道吗,如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器,感觉影响不会很大啊
这更是谬误. 需要注意的是 add操作包含锁和数组复制操作, 相对于读操作要花费更多的时间. 可以近似理解为add操作是个时间段, 而get操作是个时间点. add_start, add_end分别代表add操作时间段的开始和结束的时间点的话. 如果add_start早于get操作, 但是add_end晚于get操作, 是读取不到这一次add操作的结果的.
网友评论