并发容器学习—CopyOnWriteArrayList与CopyOnWriteArraySet

一、CopyOnWriteArrayList并发容器

1.CopyOnWriteArrayList的底层数据结构

    CopyOnWriteArrayList的底层实现是数组,CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其增删改操作都是通过对底层数组的重新复制来实现的。这种容器内存开销很大,但是在读操作频繁(查询),写操作(增删改)极少的场合却极用,并且每一次写操作都是在一块新的空间上进行的,不存在并发问题。

 

2.CopyOnWriteArrayList的继承关系

    CopyOnWriteArrayList继承关系如下图所示,List接口和Collection接口在学习ArrayList时已经分析过,不在多说。

24b0b16c98766bcd6a95e9f15fbf952d950.jpg

3.重要属性和构造方法

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

    //可重入锁
    final transient ReentrantLock lock = new ReentrantLock();

    //底层实现,用于存放数据的数组
    private transient volatile Object[] array;

    //获取数组
    final Object[] getArray() {
        return array;
    }

    //修改数组
    final void setArray(Object[] a) {
        array = a;
    }

    //默认构造器,初始化一个长度为0 的数组
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    //将集合c中所有元素转化成数组的形式,存储与当前List中
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

    //将数组toCopyIn拷贝以一份到array中
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
}

4.add与set过程

    CopyOnWriteArrayList在新增元素时,会先进行加锁操作,以保证写操作的并发安全

//将新增数据加到数组末尾
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();    //解锁
    }
}

//新增元素到指定位置
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;    //获取重入锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)    //判断索引是否合法
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        //判断插入的位置是否是数组末尾
        if (numMoved == 0)    
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            //将原数组中0到index的元素复制到新数组的0到index位置
            System.arraycopy(elements, 0, newElements, 0, index);
            //将原数组的index之后的数据复制到新数组的index+1位置之后
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;    //新增element
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

//修改index索引上的数据
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();
    }
}

5.get的过程

    CopyOnWriteArrayList的读取过程并没加锁,这是因为CopyOnWriteArrayList的读过程是天然线程安全的,所有的写操作都是在一块新的内存空间上,而读操作则是在原有的空间上进行的,不会出现并发问题,读写天然分离。

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

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

6.remove的过程

    删除的过程其实以新增修改一样,都是新建数组实现的。

//根据索引删除数据
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();
    }
}

//将o从数组中删除
public boolean remove(Object o) {
    Object[] snapshot = getArray();
    int index = indexOf(o, snapshot, 0, snapshot.length);    //获取o的索引位置
    
    //判断index是否合法
    return (index < 0) ? false : remove(o, snapshot, index);
}

//将snapshot中index位置的数据o删除
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: {
            //比较修改后的数组长度与index索引的大小,取小的值
            int prefix = Math.min(index, len);

            //遍历current数组查找第一个与快照数组元素不同的索引
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }

            //o元素索引大于等于current数组的长度
            //说明current数组中不存在o元素
            if (index >= len)
                return false;

            //o元素的索引仍在current数组中
            //且要删除元素的索引的没有发生改变
            if (current[index] == o)
                break findIndex;

            //o元素索引发生改变,重新获取o元素的索引
            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();
    }
}

7.总结

    由上面的分析可得出CopyOnWriteArrayList具有如下特点:

    1.CopyOnWriteArrayList是线程安全的容器,读写分离,写数据时通过ReentrantLock锁定一个线程拷贝数组元素进行增删改操作;读数据时则不需要同步控制。

    2.CopyOnWriteArrayList中是允许null数据和重复数据的

    3.CopyOnWriteArrayList的并发安全性是通过创建新的数组和重入锁实现的,会耗费大量内存空间,只适合读多谢写少,数据量不大的环境。

 

二、CopyOnWriteArraySet并发容器

1.CopyOnWriteArraySet

    由于CopyOnWriteArraySet的底层是通过CopyOnWriteArrayList来实现的,其特点与CopyOnWriteArrayList,因此不再做过多的分析。

 

 

转载于:https://my.oschina.net/bzhangpoorman/blog/3044787

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值