Java并发-CopyOnWriteArrayList

简述

ArrayList不是一个线程安全的类,在我们开发中要让List线程安全可能会用到Vector,而Vector是直接在方法上用synchronized关键字实现线程同步的的,性能有比较大的问题,并发包中提供了CopyOnWriteArrayList,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略。性能会更好。

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    //独占锁,后面发生对list的操作都要用到
    final transient ReentrantLock lock = new ReentrantLock();

    //存放具体的元素
    private transient volatile Object[] array;

    //...

}

lock和array是CopyOnWriteArrayList中两个比较重要的属性。

CopyOnWriteArrayList涉及修改的方法

CopyOnWriteArrayList中涉及到修改的方法都是要先拷贝一个数组,在拷贝数组上进行操作,之后再用拷贝的数组替换原本的数组。

// 用于修改数组中的元素
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 {
            //要更新的值如果和旧值是一样的,其实我们可以不用进行操作,这里还是重新设置了一下Array,是为了保证volatile语义
            setArray(elements);
        }
        return oldValue;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
// 在数组中添加元素
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 拿到独占锁
    lock.lock();
    try {
        // 拿到存放元素的数组
        Object[] elements = getArray();
        int len = elements.length;
        // 拷贝出一个比元素组长度多1的新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 新数组中插入元素
        newElements[len] = e;
        // 用新数组替换旧的数组
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}
// 数组中删除元素
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();
    }
}

每一个涉及到变动数组的操作都要先获取锁,这样是为了保证同一个时刻只有一个线程在修改数组。而每一次更新的错都是操作拷贝的数组,这样原本在就数组上进行的查询、迭代就可以不受影响,这就是写时复制。

获得元素

// 获取元素的入口
public E get(int index) {
    return get(getArray(), index);
}

// 获得集体元素
private E get(Object[] a, int index) {
    return (E) a[index];
}

//获得存放元素的数组
final Object[] getArray() {
    return array;
}

通过代码可以看到获取元素时分成两个步骤的:

  1. 获取数组
  2. 通过下标获取具体的元素

如果没有进行处理,那么这样的步骤在并发下就有可能出现问题,但是由于发生变动的操作都是同步的,并且时写时复制的,所以就算在第一步第二步直接发生了数据变化,查询也不会被影响到。(因为操作的是修改之前的数组,而变更操作更新的都是复制之后的新数组)

总结

CopyOnWriteArrayList使用写时复制的策略来保证list的一致性,而获取―修改—写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改。另外在查询期间,其他线程对list的修改是不可见的,查询的数组是一个快照。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值