java容器——CopyOnWriteArrayList/Set

字数 609阅读 20

夯实基础系列:
基础是否扎实,决定你是否能走的更稳更远。


Copy-On-Write

Copy-On-Write(COW),写时复制,是一种程序优化策略:即所有人共享同一个对象,当某个线程修改时,需要将内容copy一份进行修改。修改后,所有线程访问修改后的对象。

这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器:CopyOnWriteArrayList和CopyOnWriteArraySet。

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

主要方法

访问类方法——无锁访问

访问类方法,主要包括get(), indexof(), contains(), empty(), size()等方法

   /**
     * The lock protecting all mutators.  (We have a mild preference
     * for builtin monitors over ReentrantLock when either will do.)
     */
    final transient Object lock = new Object();

    /** The array, accessed only via getArray/setArray. */
    // Android-changed: renamed array -> elements for backwards compatibility
    private transient volatile Object[] elements;

    final Object[] getArray() {
        return elements;
    }

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

    /**
     * @throws IndexOutOfBoundsException
     */
    public E get(int index) {
        return get(getArray(), index);
    }

    private static int indexOf(Object o, Object[] elements, int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;  //允许有null的
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }


(TODO,这里留个小坑,java对象相等的判断)

修改类方法——加锁、复制

修改类的方法包括:set(), add(), remove(), removeRange(), addIfAbsent()等方法。

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);
        } 
        return oldValue;
    } finally {
        lock.unlock();  
       //这里加锁用到了lock,为什么不用Sychronized呢?当然为了优化性能了
    }
}

/**
 * Removes the element at the specified position in this list.
 * Shifts any subsequent elements to the left (subtracts one from their
 * indices).  Returns the element that was removed from the list.
 * @throws IndexOutOfBoundsException
 */
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)
            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();
    }
}

存在的问题

内存占用问题

copy-on-write采用写时复制的策略,所以在写操作的时候,内存中会同时存在两个对象,当内存占用高的时候,会导致yong gc或full gc。原来对象占用内存200M,新写入100M。内存中的对象300M?500M?
同时这也提醒在使用copy-on-write机制时,注意不应保存过大的对象。

数据一致性问题

copy-on-write只能保证数据的最终一致性,不能保证数据的实时一致性。

CopyOnWrite的应用场景

CopyOnWrite并发容器用于读多写少的并发场景,比如白名单黑名单之类。

课后思考题:

1、还记得对源容器加锁是怎么实现的吗?为什么不用Sychronized?

参考:
http://ifeve.com/java-copy-on-write/

推荐阅读更多精彩内容