JavaSE(14)——CopyOnWriteArrayList

CopyOnWriteArrayList解析

1. Vector和SynchronizedList

1.1 回顾

​ 众所周知,ArrayList是用于替代Vector的,Vector是线程安全的容器,因为它几乎在每个方法声明处都加了synchronized关键字来使容器安全。

​ 如果使用Collections.synchronizedList(new ArrayList())来使得ArrayList线程安全的话,也是几乎在每个方法上都加上了synchronized关键字,不同的是,它是加在方法内部,而不是方法声明上。

1.2 可能存在的问题

​ 不管是使用Vector还是使用SynchronizedList,在多个线程中对该对象进行操作时,都会发生线程安全问题。唯一的解决办法就是在线程中操作对象之前,给该集合对象加锁,但是这样每次进行操作时都需要加锁,程序的效率会大打折扣。

​ 因此,就需要使用CopyOnWriteArrayList

2. CopyOnWriteArrayListSet介绍

​ 一般来说,我们都认为:CopyOnWriteArrayList是同步List的替代品,CopyOnWriteArrayListSet是同步Set的替代品。

​ 无论是HashTable==》ConcurrentHashMap,还是Vector==》CopyOnWriteArrayList。JUC下支持并发的容器与老一代的线程安全类相比,总结起来就是加锁粒度的区别。

  • HashTable、Vector加锁的粒度大,直接在方法声明处使用synchronized。
  • ConcurrentHashMap、CopyOnWriteArrayList加锁粒度小 (用各种方式来实现线程安全,比如ConcurrentHashMap使用了cas锁、volatile等方法实现线程安全)
  • JUC下的线程安全容器在遍历的时候不会抛出ConcurrentModificationException异常

​ 所以一般来说,我们都会使用JUC包下提供的线程安全容器,而不是老一代的线程安全容器。

3. CopyOnWriteArrayList实现原理

如果有多个调用者 (callers) 同时请求相同资源 (如内存或磁盘上的数据存储) ,他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用的副本给该调用者,而其他调用者所见到的最初的资源仍然保持不变。

优点是如果调用者没有修改该资源,就不会有副本被建立,因此多个调用者只是读取操作时可以共享同一份资源

  • CopyOnWriteArrayList是线程安全容器 (相对于ArrayList) ,底层通过复制数组的方法来实现。
  • CopyOnWriteArrayList在遍历的使用不会抛出ConcurrentModificationException异常,并且遍历的时候就不用额外加锁
  • 元素可以为null

4. CopyOnWriteArrayList源码

4.1 属性和构造方法

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 	
    //重入锁
    final transient ReentrantLock lock = new ReentrantLock();
    //存储数组,只能通过getArray/setArray访问
    private transient volatile Object[] array;
    //获取数组
    final Object[] getArray() {
        return array;
    }
    //设置数组
    final void setArray(Object[] a) {
        array = a;
    }
    //初始化数组 设置一个空数组
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
}

4.2 常见方法

根据线程安全分析,如果遍历Vector/SynchronizedList是需要自己手动加锁的。

CopyOnWriteArrayList使用迭代器遍历时不需要显式加锁。

  • 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();
        }
    }
    
  • size方法

    public int size() {
        return getArray().length;
    }
    
  • get方法

    public E get(int index) {
        return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
    	return (E) a[index];
    }
    
  • 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;
                //操作的是副本数组,此时get方法不受影响
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                //元素相同,无操作
                setArray(elements);
            }
            return oldValue;
        } finally {
            
            //完成操作后解锁
            lock.unlock();
        } 
    }
    
  • 对于remove,clear方法跟set,add类似

总结

  • 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将新数组交由array变量指向。
  • 写加锁,读不加锁

4.3 迭代方法

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

//迭代器成员属性
//被迭代数组
private final Object[] snapshot;
//被返回元素的下标
private int cursor;

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

迭代器中的方法都是基于snapshot数组进行的,而snapshot是被传入的原数组,因此,CopyOnWriteArrayList在用迭代器遍历时,操作的都是原数组

在多线程情况下,其他线程执行clear等加锁操作时,会创建一个副本数组,进行操作,不影响原数组

4.4 缺点

  • 内存占用:如果CopyOnWriteArrayList经常要增删改里面的数据的话,经常要执行add,set,remove方法,这是比较耗费内存的。
    • 这几个方法都是要复制一个数组出来的
  • 数据一致性:CopyOnWriteArrayList容器只能保证数据的最终一致性,不能保证数据的实时一致性
    • 当线程A在迭代CopyOnWriteArrayList容器中的数据,此时线程B在迭代中进行了增删改操作,调用了setArray方法,数据已经修改了,但是线程A迭代出来的还是原有的数据。

5. CopyOnWritSet

​ CopyOnWriteSet的底层就是CopyOnWriteArrayList

private final CopyOnWriteArrayList<E> al;

public CopyOnWriteArraySet() {
    al = new CopyOnWriteArrayList<E>();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值