CopyOnWriteArrayList源码分析

介绍:

CopyOnWriteArrayList 是一个线程安全的 ArrayList,它在每次修改(add/set/remove)时创建数组的新副本,然后将修改应用到新数组上。这是它名字的由来:"CopyOnWrite"。

这种设计使得它在多线程环境下能提供更好的并发性能。当一个线程修改列表时,其他线程不能访问旧数组,因此不会受到数据不一致的影响。然而,写操作的代价是创建新数组并复制所有元素,这可能在大量写操作的情况下导致性能下降。

需要注意的是,由于 CopyOnWriteArrayList 在修改操作时复制整个数组,所以它不适用于处理大量写操作和/或大数组的情况,因为这种情况下,复制操作可能会导致显著的性能下降。在这些情况下,你可能需要考虑其他并发控制策略,例如使用锁或者使用并发集合的其他实现。

属性:

复制代码
/** The lock protecting all mutators */ 锁对象 用于解决线程竞争
final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */ 存放数据
private transient volatile Object[] array;

构造方法:

无参构造 :初始化array

java 复制代码
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

有参构造: 传入一个集合

java 复制代码
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class) 如果是当前类 则直接获取array属性的值
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        //这里的是JDK以前的以一个bug 有时候虽然是toArray方法返回的虽然是Object[]数组 但实际的类型仍然是原本的类型,这种情况放入object对象会报错 ,所以重新复制了一个新的Object数组对象
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

有参构造:传入数组

java 复制代码
  //复制的原因和上一个构造方法一样
    
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

常用方法:

get:

分为两步 :

1.第一步通过getArray()获取的array属性

2.取出元素

需要注意的是该方法并未加锁,也就是说,在第一步执行的完的时候,如果有其他线程将array的值修改了,此时get的获取的array属性还是旧的引用,是无法感知到新的的变化的,也就是弱一致性。

java 复制代码
public E get(int index) {
        return get(getArray(), index);
    }

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

add:

核心正如类名一样 先copy再write,同时对get的弱一致性有所了解了把。

java 复制代码
  public boolean add(E e) {
//获取锁对象 
        final ReentrantLock lock = this.lock;
//加锁
        lock.lock();

        try {
//获取array属性
            Object[] elements = getArray();
//获取array的长度
            int len = elements.length;
//将array的值复制成一个新的数组 并且长度+1 填充null
            Object[] newElements = Arrays.copyOf(elements, len + 1);
//将对应的下表复制
            newElements[len] = e;
//将新数组赋给array属性
            setArray(newElements);

            return true;
        } finally {
//解锁
            lock.unlock();
        }
    }

重载方法

java 复制代码
 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)
 //复制数组并len加1 
                newElements = Arrays.copyOf(elements, len + 1);
            else {
//创建一个长度为len+1的数组
                newElements = new Object[len + 1];
//将旧数组的数据从0开始 复制到新数组的下标 也从0开始  长度为index个
                System.arraycopy(elements, 0, newElements, 0, index);
//将旧数组从index开始复制到新数组 从index+1为值开始的数据复制 复制长度为numMoved个
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
//将对应下标的值替换 并替换旧数组
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }

set:

set方法中有意思的地方是,旧值和新值一样的情况下,还会重新赋值一次,jdk的注释是为了保证volatile语义。

volatile保证了不同线程之间对共享变量操作时的可见性,也就是当一个线程修改volatile修饰的变量,另一个线程会立即看到该结果

个人理解:是不同线程操作该对象的set方法或其他方法,set的结果应对后另一个线程可见,若不进行赋值,没有volatile写,其他线程的cpu缓存的array属性不会失效,不符合volatile语义。这块争议比较大,在jdk11曾经移除过,后来又加了回来,若有不同理解,可以一起讨论。

java 复制代码
   public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();

        try {

//获取array属性
            Object[] elements = getArray();
//获取对应的下标的值
            E oldValue = get(elements, index);

//如果旧和新的值不一样 
            if (oldValue != element) {
//复制数组并在新数组中替换对应下表的值,再赋给array
                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
//注释为:为了保住volatile语义 有了旧的赋值动作
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

remove:

和新增元素的代码类似,首先获取独占锁以保证删除数据期间其他线程 不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组, 之后使用新数组替换原来的数组,最后在返回前释放锁

java 复制代码
 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)
 //等于0就是末尾的位置 直接复制新数组 长度为旧的减一并赋值
                setArray(Arrays.copyOf(elements, len - 1));

            else {
//创建新数组 长度为旧的-1
                Object[] newElements = new Object[len - 1];
//将旧的数组复制到新数组 从下标0开始 长度为 index (左闭右开)
                System.arraycopy(elements, 0, newElements, 0, index);
//将旧的数组复制到新数组 从旧下标index+1开始 在新的index开始 长度为numMoved
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
//替换旧数组
                setArray(newElements);
            }

            return oldValue;
        } finally {
            lock.unlock();
        }
    }

重载方法:删除集合中第一个元素

java 复制代码
  public boolean remove(Object o) {
        //获取数组
        Object[] snapshot = getArray();
        //获取第一次出现的下标
        int index = indexOf(o, snapshot, 0, snapshot.length);

        return (index < 0) ? false : remove(o, snapshot, index);
    }

    
    private boolean remove(Object o, Object[] snapshot, int index) {
        //加锁
        final ReentrantLock lock = this.lock;
        lock.lock();

        try {
//获取当前array属性
            Object[] current = getArray();
            int len = current.length;
//如果传入的数组和当前的不一致
            if (snapshot != current) findIndex: {
                
//防止数组越界
                int prefix = Math.min(index, len);
findIndex类似一种方法体,后面break的实际跳出的是当前方法
                for (int i = 0; i < prefix; i++) {
//找到两个数组内下标一样内容不一致的位置 并且o和当前数组位置的内容的相等
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
//获取在当前数组内的下标 跳出方法体
                        index = i;
                        break findIndex;
                    }

                }
//执行到此处说明没在循环内找到
//如果该位置大于数组长度 返回false
                if (index >= len)
                    return false;
//如果当前位置元素==0 跳出findIndex方法体 不再执行后面逻辑
                if (current[index] == o)
                    break findIndex;
                //获取o在current中的位置 如果没找到 返回false
                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();
        }
    }

iterator:

返回的是内部类的对象 此类实现了ListIterator接口,该接口继承自Iterator。需要注意的是getArray()方法返回的是当前一刻的array,若之后array被替换成新的,那么在迭代过程中的修改 是不会影响到新数组的,也就是通常说的弱一致性迭代。

java 复制代码
public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

总结:

CopyOnWriteArrayList 使用写时复制的策略来保证 list的一 致性,而获取-修改-写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有1个线程能对 list 数组进行修改,另外 copyOnWriteArrayList 供了弱一致性的法代 从而保证在获取迭代器后,其他线程对 list 修改是不可见的,迭代器遍历的数组是 1个快照。另外CopyOnWriteArraySet 的底层就是使用它实现的,在保证不会新增同样的元素时调用的是addIfAbsent方法(不存在则增加),有兴趣的可以翻阅源码。

复制代码
相关推荐
F-2H2 分钟前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05675 分钟前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i1 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx1 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
bryant_meng1 小时前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩
武子康2 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
若亦_Royi2 小时前
C++ 的大括号的用法合集
开发语言·c++
资源补给站3 小时前
大恒相机开发(2)—Python软触发调用采集图像
开发语言·python·数码相机
豪宇刘3 小时前
MyBatis的面试题以及详细解答二
java·servlet·tomcat
秋恬意3 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis