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

最后

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

Java3y

你可能感兴趣的:(CopyOnWrite 详解)