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方法(不存在则增加),有兴趣的可以翻阅源码。

复制代码
相关推荐
吾日三省吾码14 分钟前
JVM 性能调优
java
stm 学习ing19 分钟前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc1 小时前
《Python基础》之字符串格式化输出
开发语言·python
弗拉唐1 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi772 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
mqiqe2 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin2 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
少说多做3432 小时前
Android 不同情况下使用 runOnUiThread
android·java