• CopyOnWriteArrayList源码分析


    其中唯一的线程安全 List 实现就是 CopyOnWriteArrayList。

    特点

    由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 List 的内部数据,毕竟对于读取操作来说是安全的。
    这种思路与 ReentrantReadWriteLock 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。
    CopyOnWriteArrayList 更进一步地实现了这一思想。为了将读操作性能发挥到极致,CopyOnWriteArrayList 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。

    CopyOnWriteArrayList 线程安全的核心在于其采用了 写时复制(Copy-On-Write) 的策略

    思想

    写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。

    当需要修改( add,set、remove 等操作) CopyOnWriteArrayList 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。

    写时复制缺点

    1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
    2. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
    3. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。

    结构

    在这里插入图片描述

    add

    // 插入元素到 CopyOnWriteArrayList 的尾部
    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);
            // 元素放在新数组末尾
            newElements[len] = e;
            // array指向新数组
            setArray(newElements);
            return true;
        } finally {
            // 解锁
            lock.unlock();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • add方法内部用到了 ReentrantLock 加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。
    • CopyOnWriteArrayList 通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。采用了 写时复制(Copy-On-Write) 的策略。
    • 每次写操作都需要通过 Arrays.copyOf 复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,CopyOnWriteArrayList 适用于读多写少的场景。
    • CopyOnWriteArrayList 中并没有类似于 ArrayList 的 grow() 方法扩容的操作。

    读取元素

    CopyOnWriteArrayList 的读取操作是基于内部数组 array 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。

    // 底层数组,只能通过getArray和setArray方法访问
    private transient volatile Object[] array;
    
    public E get(int index) {
        return get(getArray(), index);
    }
    
    final Object[] getArray() {
        return array;
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    get方法是弱一致性的,在某些情况下可能读到旧的元素值。

    获取列表中元素的个数

    CopyOnWriteArrayList中的array数组每次复制都刚好能够容纳下所有元素,并不像ArrayList那样会预留一定的空间。因此,CopyOnWriteArrayList中并没有size属性CopyOnWriteArrayList的底层数组的长度就是元素个数,因此size()方法只要返回数组长度就可以了。

    删除

    public E remove(int index) {
        // 获取可重入锁
        final ReentrantLock lock = this.lock;
        // 加锁
        lock.lock();
        try {
        	   //获取当前array数组
            Object[] elements = getArray();
            // 获取当前array长度
            int len = elements.length;
            //获取指定索引的元素(旧值)
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            // 判断删除的是否是最后一个元素
            if (numMoved == 0)
            	   // 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                // 分段复制,将index前的元素和index+1后的元素复制到新数组
                // 新数组长度为旧数组长度-1
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                //将新数组赋值给array引用
                setArray(newElements);
            }
            return oldValue;
        } finally {
           	// 解锁
            lock.unlock();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    判断元素是否存在

    // 判断是否包含指定元素
    public boolean contains(Object o) {
        //获取当前array数组
        Object[] elements = getArray();
        //调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
        return indexOf(o, elements, 0, elements.length) >= 0;
    }
    
    // 判断是否保证指定集合的全部元素
    public boolean containsAll(Collection<?> c) {
        //获取当前array数组
        Object[] elements = getArray();
        //获取数组长度
        int len = elements.length;
        //遍历指定集合
        for (Object e : c) {
            //循环调用indexOf方法判断,只要有一个没有包含就直接返回false
            if (indexOf(e, elements, 0, len) < 0)
                return false;
        }
        //最后表示全部包含或者制定集合为空集合,那么返回true
        return true;
    }
    
    private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
        if (o == null) {
            for (int i = index; i < fence; i++)
                if (elements[i] == null)
                    return i;
        } else {
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    作者声明

    如有问题,欢迎指正!
    
    • 1
  • 相关阅读:
    对汉诺塔n=4的情况进行解析
    Vuejs中字符串判空处理--如何判断字符串是否为空或不为空
    子类加@Data后,IDEA调试时“出现”父类属性无值
    IDEA操作Sharding-JDBC实战2
    parallels desktop 19密钥分享 附PD虚拟机安装教程 支持M/intel
    python、ruby、go、java写的端口扫描工具
    fl studio21最新汉化破解版中文解锁下载完整版本
    Uniapp零基础开发学习笔记(4) -顶部导航栏titleNView的制作
    安卓机调用 audio.play()时 报错:API can only be initiated by a user gesture
    Android组件化开发,其实就这么简单
  • 原文地址:https://blog.csdn.net/weixin_45247019/article/details/132865191