• CopyOnWriteArrayList源码分析


    目录

    一、概述

    二、类图

    三、核心方法

    1.add()

    2.set()

    3.remove()

    4.get()

    5.size()

    四、总结


    一、概述

            CopyOnWriteArrayList是基于写时复制技术实现的,适用于读多写少场景下的线程安全的并发容器。读操作永远不会加锁,读读、读写都不会冲突,只有写写需要等待。写操作时,为了不影响其它线程的读取,它会进行一次自我复制,待数据写入完成后再替换array数组。array数组是被volatile修饰的,它被修改后可以被其他线程立刻发现。

    二、类图

    image.png

    •  实现了RandomAccess接口,代表它支持快速随机访问,因为它底层数据结构是数组,支持通过下标快速访问;
    • 实现了Cloneable接口,代表它支持克隆,使用的是浅拷贝模式;
    • 实现了List接口,代表它是一个有序的列表容器,支持迭代遍历等操作。

    三、核心方法

    1.add()

            向容器中添加元素时,需要竞争锁,同一时刻最多只有一个线程可以操作。因为是写时复制,写入数据时不应该影响其他线程的读取,因此不会直接在array数组上操作,而是拷贝一个新的数组,元素设置完成后再覆盖旧数组。

    1. public boolean add(E e) {
    2. final ReentrantLock lock = this.lock;
    3. lock.lock();
    4. try {
    5. Object[] elements = getArray();
    6. int len = elements.length;
    7. // 拷贝一个长度+1的数组,将元素放到末尾
    8. Object[] newElements = Arrays.copyOf(elements, len + 1);
    9. // 填充要追加的元素e
    10. newElements[len] = e;
    11. // 覆盖旧数组
    12. setArray(newElements);
    13. return true;
    14. } finally {
    15. lock.unlock();
    16. }
    17. }

    2.set()

            set方法用来给指定下标设置值,同时会返回旧值。它也是一个写入操作,因此也需要竞争到锁才能执行。为了不影响其它线程读取,它会拷贝一个同样长度的新数组,然后做数据拷贝,在新数组上完成新值的设置,最终再写回array。

    1. public E set(int index, E element) {
    2. final ReentrantLock lock = this.lock;
    3. lock.lock();
    4. try {
    5. Object[] elements = getArray();
    6. // 先获取旧元素
    7. E oldValue = get(elements, index);
    8. if (oldValue != element) {
    9. int len = elements.length;
    10. // 拷贝一个一样的数组,替换下标元素,并写入array
    11. Object[] newElements = Arrays.copyOf(elements, len);
    12. newElements[index] = element;
    13. setArray(newElements);
    14. } else {
    15. // 即使元素没有变化,也要写入array,确保volatile的写语义
    16. // Not quite a no-op; ensures volatile write semantics
    17. setArray(elements);
    18. }
    19. return oldValue;
    20. } finally {
    21. lock.unlock();
    22. }
    23. }

    3.remove()

            remove也是写操作,只有竞争到锁的线程才能执行。它先是取出对应下标的旧元素,然后新建了一个原数组长度减1的新数组,完成数据拷贝后,再写回array,整个过程依然不影响其它线程读。

    1. public E remove(int index) {
    2. final ReentrantLock lock = this.lock;
    3. lock.lock();
    4. try {
    5. Object[] elements = getArray();
    6. int len = elements.length;
    7. // 要移除的旧元素
    8. E oldValue = get(elements, index);
    9. int numMoved = len - index - 1;
    10. if (numMoved == 0)
    11. // 删除的是最后一个元素,直接拷贝一个长度-1的数组写回array即可
    12. setArray(Arrays.copyOf(elements, len - 1));
    13. else {
    14. // 删除的是中间元素,拷贝一个长度-1的数组
    15. Object[] newElements = new Object[len - 1];
    16. // 拷贝前半段元素
    17. System.arraycopy(elements, 0, newElements, 0, index);
    18. // 拷贝后半段元素
    19. System.arraycopy(elements, index + 1, newElements, index,
    20. numMoved);
    21. // 写回array
    22. setArray(newElements);
    23. }
    24. return oldValue;
    25. } finally {
    26. lock.unlock();
    27. }
    28. }

    4.get()

            通过下标获取元素,直接从array数组中取。因为是写时复制的,可能在访问时已经有新的元素加入,或者有元素被删除,这是会存在延迟的,不是实时的,这是它的一个缺点。

    1. public E get(int index) {
    2. // getArray()获取的就是array
    3. return get(getArray(), index);
    4. }
    5. private E get(Object[] a, int index) {
    6. return (E) a[index];
    7. }

    5.size()

            获取元素的数量直接取数组的长度即可。因为CopyOnWriteArrayList的数组是不可变数组,它始终是一个被填充满的数组对象,没有扩容的操作,因此也没有必要像ArrayList一样,额外使用一个int size来记录数量。

    1. public int size() {
    2. return getArray().length;
    3. }

    四、总结

            CopyOnWriteArrayList 具有以下特性:

    1.  在保证并发读取的前提下,确保了写入时的线程安全;
    2.  由于每次写入操作时,进行了Copy复制原数组,所以无需扩容;
    3.  适合读多写少的应用场景。由于 add() 、 set() 、 remove() 等修改操作需要复制整 个数组,所以会有内存开销大的问题;
    4. CopyOnWriteArrayList 由于只在写入时加锁,所以只能保证数据的最终一致性,不能 保证数据的实时一致性。
  • 相关阅读:
    Qt编写ffmpeg本地摄像头显示(16路本地摄像头占用3.2%CPU)
    【python】PyQt5可视化开发,鼠标键盘实现联动界面交互逻辑与应用实战
    电脑重置与重装系统的区别
    Ae 效果:CC Star Burst
    什么是响应式网页设计,解释响应式网页设计的原理和优势
    船舶防范有限空间作业事故办法
    HTTP2指纹识别(一种相对不为人知的网络指纹识别方法)
    (LeetCode C++)旋转图像
    【图解计算机网络】从浏览器地址输入到网页显示的整个过程
    kkfile配置https预览文件
  • 原文地址:https://blog.csdn.net/weixin_52386948/article/details/126920247