zoukankan      html  css  js  c++  java
  • CopyOnWriteArrayList设计思路与源码分析

    CopyOnWriteArrayList实现了List接口,RandomAccess,Cloneable,Serializable接口。
    在这里插入图片描述

    CopyOnWriteArrayList特性

    1、线程安全,在多线程环境下作为共享变量可以放心使用,无需加锁。
    2、通过加锁和volatile保证安全
    3、每次对数组进行增删改操作都会复制原先元素到新的数组中,在新的数组上进行操作,最后再赋值回去。

    他底层使用的数据结构也是数组,对数组中元素的操作都会经历,加锁,拷贝原数组到新数组中,对新数组进行增删改,然后赋值回旧的数组,最后解锁。

    除了这些操作外,CopyOnWriteArrayList内的array数组是被volatile和transient修饰的。
    在这里插入图片描述

    类注释

    从类注释中,我们可以知道CopyOnWriteArrayList是线程安全的集合类,因为增删改都是在新的数组中进行的,当更新完成后,新数组又被赋值给原先数组,这样所有线程都可以知道哪些元素被修改了。
    虽然数组的拷贝开销较大,但是往往比常用的方案效率要好。
    在迭代过程中,不会抛出ConcurrentModificationException,因为并不是在原先数组上修改的。

    新增

    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);
                // 直接将新元素e赋值给新数组的最后
                newElements[len] = e;
                // 设置给原数组array,array = newElements
                setArray(newElements);
                return true;
            } finally {
            // 最后释放锁
                lock.unlock();
            }
        }
    

    在整个add的过程中都是加锁的,所以在同一时刻只有一个线程可以add成功,既然已经加锁,那为什么还要创建新数组进行拷贝呢?这是因为原先数组是volatile修饰的,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行。通过新建一个数组拷贝旧的数组,是可以避免在赋值过程中出现旧数组值被改变的情况。

    当在指定位置插入时,如果插入元素的位置时最后一个,那么只需要拷贝一次,如果在中间位置插入时,需要拷贝两次(将数组一分为二);

    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];
                    // 否则进行两次拷贝
                    System.arraycopy(elements, 0, newElements, 0, index);
                    System.arraycopy(elements, index, newElements, index + 1,
                                     numMoved);
                }
                // 给新元素赋值
                newElements[index] = element;
                // 设置新数组
                setArray(newElements);
            } 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);
                // 减一是因为len是从1开始的,index是从0开始的
                int numMoved = len - index - 1;
                if (numMoved == 0)// 如果删除的是最后一个,直接删除
                    setArray(Arrays.copyOf(elements, len - 1));
                else {
                // 如果删除的是中间元素,新建的数组大小-1,并且从0拷贝到删除元素前,从删除元素的后一个拷贝到最后
                    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();
            }
        }
    

    删除元素的思路就是
    1、加锁
    2、根据删除元素的下标确定在数组中的位置,然后采取不同的策略删除
    3、解锁
    不管事新增还是删除,lock对象都是被final修饰的,他们都共用一把锁,try finally+数组拷贝保证了能够成功删除指定元素。

    小结

    该集合类的优点是:读取元素不需要加锁,适合读多写少的场景。例如:读取白名单,黑名单等。
    缺点也很明显:内存开销比在原数组上修改多了一倍内存占用,可能导致年轻代GC,如果要求强一致性就不能使用这个了。

  • 相关阅读:
    springMVC中@RequestParam和@RequestBody的作用
    java 中Excel的导入导出
    数据库字段名称与实体类属性不一致的处理措施
    linux下解压命令大全
    linux 下 cat
    mysqli
    解析php mysql 事务处理回滚操作(附实例)
    css 样式(checkbox开关、css按钮)
    cetons 怎么强制卸载 PHP
    yhdsir@function:php
  • 原文地址:https://www.cnblogs.com/dataoblogs/p/14121867.html
Copyright © 2011-2022 走看看