CopyOnWrite 详解

为什会有 Copy On Write

COW 在不同的操作系统,或者框架中都会有相应的实现

优点

  • COW 技术可以减少分配和赋值大量资源带来的瞬时延迟
  • COW 可以减少不必要的资源分配。比如 fork 进程时,并不是所有的页面都需要赋值。父进程的代码段和只读数据段都不被允许修改,所以无需复制

缺点

  • 如果在 fork 之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误,这样就得不偿失

回顾

我们都知道 ArrayList 是用于替代 Vector,Vector 是线程安全的容器。因为它几乎在每个方法声明处都加了 synchronized 关键字来保证容器安全。

如果使用 Collections.synchronizedList(new ArrayList()) 来使 ArrayList 变成线程安全的话,也就是每个方法都加上 synchronized 关键字,只不过不是在方法的声明处,而是在方法的内部

下面有一段代码

public static Object getLast(Vector list){
    int lastIndex = list.size() - 1;
    return list.get(lastIndex);
}

public static void deleteLast(Vector list){
    int lastIndex = list.size() - 1;
    return list.remove(lastIndex);
}

在多线程下是否有问题

答案是有的,因为这两个方法并不是原子性的,要保证这个线程安全不能仅仅给方法加锁,还要在遍历前给 vector 加锁。在遍历中,假设对 vector 的结构进行了破坏,例如 clear,则后续的操作可能并没有第一时间可见,继续进行操作,例如 get 读取数据,从而造成程序异常,所以最好的方法是遍历前个给 vector 加锁。

什么是 COW

多个调用者同时请求相同的资源,它们会共同获取相应的指针指向相同的资源,知道某个调用者试图修改资源内容时,系统才会真正赋值一个专用副本给调用者,而其他调用者所见到的最初的资源任然保持不变。

在 Java 中 COW 的一个应用就是 CopyOnWriteArrayList

  • CopyOnWriteArrayList 相对于 ArrayList 线程安全,底层通过复制数组的方式来实现
  • 在遍历使用时不会抛出 ConcurrentmodificationException 并且便利的时候就不用额外加锁
  • 元素可以为 null

Java 中 CopyOnWriteArrayList 的实现

在 CopyOnWriteArrayList 中

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}

对于 get 方法,直接返回数据

而对于 add 方法

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();
    }
}

clear 方法

public void clear() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        setArray(new Object[0]);
    } finally {
        lock.unlock();
    }
}

set 方法

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 {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

都要进行上锁,add 和 set 方法中还执行了 ArrayCopy 方法,进行拷贝,返回的是一个副本。

为什么在遍历时不需要显式加锁

查看源码发现 CopyOnWriteArrayList 实现 Iterator 的具体是 COWIterator

@SuppressWarnings("unchecked")
public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}

在返回数据的时候是 snapshot 这个数组中的数据就,这个数组是快照(snapshot)吗?

private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

很明显不是的,构造函数中传入的就是原来的那个数组

CopyOnWriteArrayList你都不知道,怎么拿offer?

原文在这里,我还是不太明白

想了想,解释一下

因为对 List 具有结构性改变的操作都是按照 COW 实现的,即都是对数组的副本进行操作。

而我们遍历的时候用的是源本的数组,所以遍历的时候并不会造成影响

这篇文章也解决了我一直在意的一件事,为什么在对表遍历的时候,会抛出异常

其 COWIterator 的内部类实现如下

/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code remove}
 *         is not supported by this iterator.
 */
public void remove() {
    throw new UnsupportedOperationException();
}
/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code set}
 *         is not supported by this iterator.
 */
public void set(E e) {
    throw new UnsupportedOperationException();
}
/**
 * Not supported. Always throws UnsupportedOperationException.
 * @throws UnsupportedOperationException always; {@code add}
 *         is not supported by this iterator.
 */
public void add(E e) {
    throw new UnsupportedOperationException();
}

全部抛出不支持操作异常,怪不得

CopyOnWriteArrayList 的缺点

CopyOnWriteArrayList 同样有 COW 的缺点,本文开始就说了,只不过那时在操作系统中

  • COW 会造成数据错误,不能实时保证数据一致性,但是可以保证最终一致性,可以保证最终一致性

    例如一个线程 get 了一个 value 走了,另外一个进去 remove 了同一个 value,

    实时上这个里面没有这个 value,但别的线程继续拿着这个 value 进行处理。

  • 因为设计表结构的操作都要 copy,所以会造成内存占用偏高

CopyOnWriteArraySet

两者原理相同

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}

最后

推荐这篇文章的原本博主 Java3y,感觉真的很强,看他的文章收获也很高

Java3y

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277