CopyOnWriteArrayList源码分析

CopyOnWriteArrayList源码分析

作用

CopyOnWriteArrayList(Copy-On-Write: 写入时复制)是一个线程安全的ArrayList,对其进行的修改操作都是先加锁然后在底层的一个复制数组上进行。

优点: 经常被用于"读多写少"的并发场景,因为读取的时候不需要加锁,性能较好。读写分离,在使用迭代器迭代的时候不会抛出异常

缺点: 需要拷贝原数据,数据较大的时候容易引起频繁Full GC;写和读在不同的数组上,读取的是老数组的数据(弱一致性问题)

成员变量

// 独占锁
final transient ReentrantLock lock = new ReetrantLock(); 

// 只能通过getArray/setArray方法来获取/修改array
private transient volatile Object[] array;

初始化

1. 创建空链表

public CopyOnWriteArrayList() {
    setArrayList(new Object[0]);
}

2. 根据给定的集合数据创建链表

public CopyOnWriteArrayList(Collections<? extends E> c) {
    Object[] elements;
    // 如果是CopyOnWriteArrayList类,直接引用
    if (c.getClass() == CopyOnWriteArrayList.class) {
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    } else {
        elements = c.toArray();
        // c.toArray可能不会返回Object[]类型
        if (elements.getClass() != Object[].class) {
            // 通过Arrays.copyOf复制数组
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
    }
    // 设置array变量为elements
    setArray(elements);
}

3. 根据数组创建链表

public CopyOnWriteArrayList(E[] toCopyIn) {
    // 上面方法的简化
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

获取元素

get(int index)

获取array[index]位置的值

**注意: **这里假设一个情景:将get分为两步: 1. 获取array、2. 读取指定下标的值。在线程x执行完第一步还未执行第二步时,线程y对链表进行了修改,那么get方法返回的还是未修改数组里对应的值。

因为对数组的修改操作都是需要加锁之后对数组进行拷贝,对拷贝数组操作后再修改array指向拷贝数组,但是此时线程x已经将getArray()获取到的数组(原数组)压入方法栈中,因此使用的是原数组的引用,即时线程y将array指向了拷贝数组也不影响原数组

public E get(int index) {
    return get(getArray(), index);
}

修改

set(int index, E element)

修改array[index]的值

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 拿到成员变量array
        Object[] elements = getArray();
        // 获取elements[index]的值
        E oldValue = get(elements, index);
		// 如果值发生了变化
        if (oldValue != element) {
        	int len = elements.length;
            // 先对elements复制一个拷贝
            Object[] newElements = Arrays.copyOf(elements, len);
            // 更新值
            newElements[index] = element;
            // 更新this.array = newElements
            setArray(newElements);
        } else {
            // 为了保证volatile语义 
            setArray(elements);
        }
        // 返回原始值
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

添加

add(E e)

添加元素到末尾

// 和上面的set差不多 流程: 先获取锁->拷贝数组->修改拷贝数组->修改原始数组引用
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;
        // 修改array引用
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

删除

remove(int index)

删除index位置的值

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // index位置的值
        E oldValue = get(elements, index);
        // 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);
            // 修改array引用
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
remove(Object o, Object[] snapshot, int index)
private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) findIndex: {
            int prefix = Math.min(index, len);
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            if (index >= len)
                return false;
            if (current[index] == o)
                break findIndex;
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        Object[] newElements = new Object[len - 1];
        System.arraycopy(current, 0, newElements, 0, index);
        System.arraycopy(current, index + 1,
                        newElements, index,
                        len - index - 1);
        setArray(newElements);
        return true;
   } finally {
        lock.unlock();
    }
}

迭代器

迭代器和get读取一样,如果读的过程中其他线程对数据进行了修改,仍然读取老数据的值

public Iterator<E> iterator() {
    // 返回的是COWIterator的实例
    return new COWIterator<E>(getArray(), 0);
}

接下来看COWIterator的具体实现

static final class COWIterator<E> implements ListIterator<E> {
   
    // 数组的拷贝
    private final Object[] snapshot;
    // 当前下标
    private int cursor;

    // 这里可以参考get(int index)方法
    // 在调用完getArray()方法后已经将原数组的引用压入到当前线程的栈中,其他线程是将array指向其他数组,对原始数组并没修改
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

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

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    // add set remove三个操作均不支持    
    public void remove() {
        throw new UnsupportedOperationException();
    }

        
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

       
    public void add(E e) {
        throw new UnsupportedOperationException();
    }
}

使用场景

假如我们有一个搜索的网站需要屏蔽一些“关键字”,“黑名单”每晚凌晨定时更新,每当用户搜索的时候,“黑名单”中的关键字不会出现在搜索结果当中,并且提示用户敏感字。

这里因为凌晨接口被访问的可能性低,CopyOnWriteArrayList只能保证数据最终一致,只要在修改数据的时候没有被读取即可

参考

  1. 深入浅出Java多线程 16 CopyOnWriteList
  2. 《Java并发编程之美》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值