CopyOnWriteArrayList源码解析

本文详细解读了CopyOnWriteArrayList的工作原理,如何通过COW思想保证线程安全,以及其优缺点。重点讲解了加、删、改操作的源码分析,适合读多写少场景,但注意内存占用和数据实时性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

作为Java开发者都应该知道工作中最常用的ArrayList集合是属于线程不安全的。在它的底层源码实现中,没有限制线程的读写操作,因此在多线程的环境下,使用ArrayList是不够合理的。
想要线程安全,那么就使用Vector或者Collections工具类中的SynchronizedList吧。但是对于这两种集合来说,它的线程安全效果的底层实现是直接使用synchronize关键字修饰方法,也就是对整个集合上了锁,同一个时刻只能有一个线程方法访问集合。毫无疑问,效率太低,得不偿失。
针对于以上的问题,CopyOnWriteArrayList就此诞生了。

Copy-On-Write(COW设计思想)

为了更好理解之后的源码,需要先了解CopyOnWriteArrayList集合中的核心思想——COW。
我们使用集合最主要的操作就是“读写”,站在“读”的角度看,即是读操作是为了获取到集合中最新的数据,需要满足数据的实时性;而站在“写”的角度看,即是写操作是为了针对集合中的最新数据进行修改。
那么在多线程的情况下,如果不做任何限制,就会导致脏数据的产生,线程不安全。那么COW就是为了解决这种情况,它的核心思想就是通过“写时复制”来实现数据的一致性,并且能够保证“读线程间不阻塞”。

简单来说,当我们往集合中添加元素的时候,并不是直接往集合中添加元素,而是先将集合copy一份新集合出来,然后再往新集合中添加元素,添加完元素之后,再将新集合替换掉原集合。当需要从集合中获取元素的时候,不需要做任何限制,因为此时的集合不会执行添加元素的操作,但是此时由于“写时复制”的原因,可能获取到的元素并不是最新的数据。
所以COW是一种读写分离的思想,通过在写操作的时候针对的是不同的集合来实现的,同时也牺牲了数据实时性的来追求数据一致性。

CopyOnWriteArrayList的重要属性

// 独占锁(保证每次只能有一个写操作访问集合)
final transient ReentrantLock lock = new ReentrantLock();

// 内部维护一个数组实现存取数据
private transient volatile Object[] array; 

final Object[] getArray() { // 获取数组
    return array;
}

final void setArray(Object[] a) { // 重新赋值数组
    array = a;
}

CopyOnWriteArrayList的常用方法源码解析

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); // 复制一个新数组,长度为原数组的长度 + 1
        newElements[len] = e; // 将数据放入新数组最后一个空位
        setArray(newElements); // 将新数组替换掉原数组
        return true;
    } finally {
        lock.unlock(); // 释放锁
    }
}

可以看到集合add添加元素的方法中有独占锁的存在,说明在每次都只有一个添加的操作能够访问集合。这样保证了写操作时候数据的一致性和实时性,但是性能方面就有所下降。

get

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

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

这个get方法就太简单了,没什么好说的。这里主要是说明,get读操作不受任何限制,读写之间没有联系,所谓读写分离。

remove

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;  // 或需要移动的索引位置numMoved(因为删除一个元素后,需要将后面的元素往前移动一位)
       	// 将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(); // 释放锁
    }
}

总结

到此,对于CopyOnWriteArrayList的源码解析结束了。其实通过这篇博文可以知道,CopyOnWriteArrayList的底层非常简单,无非就是独占锁和写时复制的结合,从而实现了读写分离的效果。但从底层实现上看,其实CopyOnWriteArrayList更适合用于读操作大于写操作的场景。

但CopyOnWriteArrayList虽然有读写分离的优点,同时也有它的缺点:

  • 占用内存:因为在写操作时候,会将原数组复制一份,然后最后将新数组替换掉旧数组。而这里的替换,仅仅只是指针的指向改变了,原数组的仍然存在于内存当中,等待GC的回收。那么在GC回收的之前这段时间里,如果有大量的写操作涌入,而且此时数组的数据量也很大了,那么这个时间的内存将会存在大量的被弃用的数组,白白占用内存空间。

  • 数据实时性问题:在本文一开始就讲过了,CopyOnWriteArrayList牺牲了数据实时性,实现了数据一致性。每次读取的数据很可能并不是最新的数据,因此对数据实时性有要求的话,不要使用CopyOnWriteArrayList。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值