Java并发编程——CopyOnWriteArrayList详解

一、 CopyOnWriteArrayList介绍

  • CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
  • CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差
  • 在迭代器上进行的元素更改操作(remove、set和add)不受支持。这些方法将抛出UnsupportedOperationException。

它相当于线程安全的ArrayList。和ArrayList一样,它是个可变数组;但是和ArrayList不同的时,它具有以下特性:

    1. 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
    1. 它是线程安全的。
    1. 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
    1. 迭代器支持hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
    1. 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。

二、 CopyOnWriteArrayList原理

    1. CopyOnWriteArrayList实现了List接口,因此它是一个队列。
    1. CopyOnWriteArrayList包含了成员lock。每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
    1. CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
  • 4.CopyOnWriteArrayList的“动态数组”机制 -- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很 低;但是单单只是进行遍历查找的话,效率比较高。
  • 5.CopyOnWriteArrayList的“线程安全”机制 -- 是通过volatile和监视器锁Synchrnoized来实现的。
  • 6.CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的 保证。
  • 7.CopyOnWriteArrayList通过监视器锁Synchrnoized来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

三、 CopyOnWriteArrayList 属性介绍

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

    /**
     * 监视器锁
     */
    final transient Object lock = new Object();

    /** 一个缓存数组,使用volatile修饰。 */
    private transient volatile Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }
}

四、 构造器

  • 无参构造,默认创建的是一个长度为0的数组
//无参构造,默认创建的是一个长度为0的数组
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
  • 参数为Collection的构造方法
//参数为Collection的构造方法
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] es;
    //判断传递的是否就是一个CopyOnWriteArrayList集合
    if (c.getClass() == CopyOnWriteArrayList.class)
        //如果是,直接调用getArray方法,获得传入集合的array然后赋值给elements
        es = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        //先将传入的集合转变为数组形式
        es = c.toArray();
        //c.toArray()可能不会正确地返回一个 Object[]数组,那么使用Arrays.copyOf()方法
        if (c.getClass() != java.util.ArrayList.class)
            es = Arrays.copyOf(es, es.length, Object[].class);
    }
    //直接调用setArray方法设置array属性
    setArray(es);
}
  • 创建一个包含给定数组副本的list
//创建一个包含给定数组副本的list
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

说明:

  • setArray()的作用是给array赋值;其中,array是volatile transient Object[]类型,即array是“volatile数组”。
  • 关于volatile关键字,我们知道“volatile能让变量变得可见”,即对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。正在由于这种特性,每次更新了“volatile数组”之后,其它线程都能看到对它所做的更新。
  • 关于transient关键字,它是在序列化中才起作用,transient变量不会被自动序列化。

上面介绍的是CopyOnWriteList的初始化,三个构造方法都比较易懂

五、添加add方法

public boolean add(E e) {
    //加锁
    synchronized (lock) {
        //获得list底层的数组array
        Object[] es = getArray();
        //获得数组长度
        int len = es.length;
        //拷贝到新数组,新数组长度为len+1
        es = Arrays.copyOf(es, len + 1);
        //给新数组末尾元素赋值
        es[len] = e;
        //用新的数组替换掉原来的数组
        setArray(es);
        return true;
    }
}
  • 1、加锁(synchronized )。保证此时只有一个线程进入
  • 2、获取原来的数组以及长度
  • 3、复制新数据,并且长度+1
  • 4、设置新数据(array使用volatile修饰,所以其他线程可见)
  • 5、返回

六、获取元素 get(int index)

使用get(i)可以获取指定位置i的元素,当然如果元素不存在就会抛出数组越界异常。

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

static <E> E elementAt(Object[] a, int index) {
    return (E) a[index];
}   

六、remove(int index)

public E remove(int index) {
    //加锁
    synchronized (lock) {
        //获取原数组
        Object[] es = getArray();
        //获取原数组长度
        int len = es.length;
        //获取原数组index处的值
        E oldValue = elementAt(es, index);
        //因为数组删除元素需要移动,所以这里就是计算需要移动的个数
        int numMoved = len - index - 1;
        Object[] newElements;
        if (numMoved == 0)
            //计算的numMoved=0,表示要删除的是最后一个元素
            //那么旧直接将原数组的前len-1个复制到新数组中,替换旧数组即可
            newElements = Arrays.copyOf(es, len - 1);
            
        //要删除的不是最后一个元素  
        else {
            //创建一个长度为len-1的数组
            newElements = new Object[len - 1];
            //将原数组中index之前的元素复制到新数组
            System.arraycopy(es, 0, newElements, 0, index);
            //将原数组中index之后的元素复制到新数组
            System.arraycopy(es, index + 1, newElements, index,
                             numMoved);
        }
        //用新数组替换原数组
        setArray(newElements);
        return oldValue;
    }
}

remove(int index)的作用就是将”volatile数组“中第index个元素删除。它的实现方式是,如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组。否则,新建数组,然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中;最后,将新数组赋值给”volatile数组“。和add(E e)一样,remove(int index)也是”在操作之前,获取独占锁;操作完成之后,释放独占是“;并且”在操作完成时,会通过将数据更新到volatile数组中“。

七、修改元素

修改也是属于写 ,所以需要获取lock,下面就是set方法的实现

public E set(int index, E element) {
    //加锁
    synchronized (lock) {
        //获取数组array
        Object[] es = getArray();
        //获取index位置的元素
        E oldValue = elementAt(es, index);
        // 要修改的值和原值不相等
        if (oldValue != element) {
            //克隆一个新数组
            es = es.clone();
            //替换元素
            es[index] = element;
        }
        // Ensure volatile write semantics even when oldvalue == element
        // 即使在oldvalue==元素的情况下也要确保易失性写入语义
        setArray(es);
        return oldValue;
    }
}

八、 迭代器

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
  • 使用了COWIterator遍历
static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    //array的快照版本
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    //后续调用next返回的元素索引(数组下标)
    private int cursor;

    //构造器
    COWIterator(Object[] es, int initialCursor) {
        cursor = initialCursor;
        snapshot = es;
    }
    
    //变量是否结束:下标小于数组长度
    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    //是否有前驱元素
    public boolean hasPrevious() {
        return cursor > 0;
    }

    //获取元素
    //hasNext()返回true,直接通过cursor记录的下标获取值
    //hasNext()返回false,抛出异常
    @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;
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code remove}
     *         is not supported by this iterator.
     */
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code set}
     *         is not supported by this iterator.
     */
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code add}
     *         is not supported by this iterator.
     */
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        final int size = snapshot.length;
        int i = cursor;
        cursor = size;
        for (; i < size; i++)
            action.accept(elementAt(snapshot, i));
    }
}

说明:COWIterator不支持修改元素的操作。例如,对于remove(),set(),add()等操作,COWIterator都会抛出异常!
另外,需要提到的一点是,CopyOnWriteArrayList返回迭代器不会抛出ConcurrentModificationException异常,即它不是fail-fast机制的!

参考:
https://blog.csdn.net/GoSaint/article/details/115234349

https://www.cnblogs.com/zhangboyu/p/7452542.html

https://www.cnblogs.com/-beyond/p/13130040.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容