CopyOnWriteArrayList源码剖析

        这JUC包里最典型的解决ArrayList线程不安全问题的数据结构,采用读写分离的思想,比Vector和Collections.synchronizedList高效。

目录

成员变量

构造函数

添加

更改操作

查询

删除


成员变量

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    //保护所有突变器的锁
    final transient ReentrantLock lock = new ReentrantLock();

    /** 数组,只能通过 getArray/setArray 访问。 */
    private transient volatile Object[] array;  
}

        这里说明底层内存数据结构和ArrayList一样,为array数组。值得注意的是,这里用了JUC包可重锁ReetrantLock,这和Synchronized关键字还是有些异同点的,它们的底层实现不一样,适用场景也不一样,后面我将出一篇专门的文章来讨论,这里只需要直到其是”锁“,用于同步即可了。volatile关键字也是为了实现多线程环境下:数组的可见性、有序性(防止内存重排)。

构造函数

 /**
     创建一个空列表。
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]); //空
    }

    /**
    创建一个包含指定集合的元素的列表,按照集合的迭代器返回的顺序。
    参数:
    c – 最初持有的元素的集合
    抛出:
    NullPointerException – 如果指定的集合为空。
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
            elements = c.toArray(); //转数组
            if (c.getClass() != java.util.ArrayList.class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

    /**
    创建一个包含给定数组副本的列表。
    参数:
    toCopyIn – 数组(此数组的副本用作内部数组)
    抛出:
    NullPointerException – 如果指定的数组为空
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

    /**
     *设置数组。
     */
    final void setArray(Object[] a) {
        array = a;
    }

        构造函数即底层array的赋值操作。

添加

 /**
    将指定的元素附加到此列表的末尾。
    参数:
    e - 要附加到此列表的元素
    返回:
    true (由Collection.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();//解锁
        }
    }

        这里可以看出由于锁的原因,多线程时不能同时对同一个CopyOnWriteArrayList对象进行add。至于它的高效性,在于读写分离,这要结合下面的查询操作来说明。

更改操作

/**
      用指定的元素替换此列表中指定位置的元素。
      抛出:
      IndexOutOfBoundsException –
     */
    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(); //解锁
        }
    }

        这里有个重点:当数据并未修改时进行了 setArray(elements);操作。

        

        为什么要出现这个呢?因为毕竟数组内容并未发生改变——这里要从volatile的可见性和有序性说起。即我们希望发生这样的事:线程a对volatile变量进行了写入,之后线程b对volatile变量进行了读操作,我们希望线程b能够发现线程a的写操作,即这时候b读取的内容要正确并且直到时间顺序是a->b。

假设a,b还有其它的操作:

int a=0;
new Thread(()->{
  a=1; //1
  cow.set(...)//2
},"a").start();

new Thread(()->{
  cow.get();//3
  int b=a;//4
},"b").start();

         即使set方法中数据未发生变化,我们也需要感知到语句2在3之前执行,这样才能保证1在4之前执行。所以 setArray(elements)正是体现了volatile可见性、有序性的作用。

查询

/**
    返回此列表中指定位置的元素。
    抛出:
    IndexOutOfBoundsException –
     */
    public E get(int index) {
        return get(getArray(), index);
    }


    /**
			获取数组。 非私有以便也可以从 CopyOnWriteArraySet 类访问。
     */
    final Object[] getArray() {
        return array;
    }

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

        从这就可以看出,查询操作就是在老数组 array上直接查询的,并且不用加锁。也就是说如果目前添加操作和修改操作正在执行的话,两者相互不干扰,只是现在读取的是添加/修改操作之前的数据。

删除

  /**
    移除此列表中指定位置的元素。 将任何后续元素向左移动(从它们的索引中减去一个)。 返回从列表中删除的元素。
    抛出:
    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();//解锁
        }
    }

         删除操作也类似,分为删除最后一个元素(一次arraycopy)和中间元素(两次arraycopy)。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值