CopyOnWriteArrayList

并发包下面,实现了一堆常用的线程安全的数据结构,多个线程访问HashMap是线程安全的,不会把HashMap里的数据改错,ConcurrentHashMap。ArrayList数据结构,内存里面,多线程要并发的访问这个数据结构

CopyOnWriteArrayList,写时复制机制的ArrayList,可以保证线程并发的安全性

csharp 复制代码
List<String> list = new CopyOnWriteArrayList<String>();
list.add("张三");
list.set(0,"李四");
list.remove(0);
构造函数 复制代码
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

CopyOnWriteArrayList其实也是底层基于数组来实现的,List数据结构,要实现各种线程安全性

css 复制代码
final void setArray(Object[] a) {
    array = a;
}
java 复制代码
private transient volatile Object[] array;

核心的底层数据结构是数组,volatile,保证 多线程读写的可见性,只要有一个线程修改了这个数组,其他的线程立马是可以读到的

ini 复制代码
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);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

CopyOnWrite:写时复制的机制,大量的写操作都是基于写时复制的机制来实现的

elements:代表的是当前CopyOnWriteArrayList内部的数组,Arrays.copyOf复制的操作,就是把当前数组复制到了新的数组里去,新的数组的长度是多少呢?len + 1,newElements,就是一个全新的数组

新数组里包含了老数组所有的元素,而且长度还多了1位

CopyOnWrite,写数据的时候,不是直接在当前的数组里写的,他是先把老数组复制到新数组里来,大小为len + 1,接着是对新数组进行更新操作

把新数组的最后一位的元素设置成要添加的元素,你的更新的操作此时都是发生在新数组里的,跟老数组是没关系的,写时复制的机制,写数据的时候,复制一个新的数组,然后在新的数组里更新元素

最后再把新的数组设置为CopyOnWriteArrayList对应的一个数组,volatile写保证说,只要他一写,其他线程可以立马读到

老数组稍后就会被jvm垃圾回收掉了,已经没有人使用他了

ini 复制代码
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            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
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

将老数组复制到一个新数组里去,新老两个数组的大小是一样的,都是len,修改一个元素,并不是删除元素,也不是新增元素

对复制之后的一个新数组的指定index位置的元素设置为element元素

他就会将修改后的新数组设置为CopyOnWriteArrayList底层的数组

ini 复制代码
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)
            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底层都对应一个数据结构,Object[]数组,同时还对应了一个ReentrantLock独占锁,就是用独占锁来保证说要修改Object[]数组的时候,必须加独占锁,此时只能有一个线程获取锁

独占锁保证了,只有一个线程可以来修改底层的数组里的数据

增删改操作的时候,都必须先获取一把ReentrantLock独占锁,保证同一时间只能有一个线程来操作底层的数组数据结构,更新CopyOnWriteArrayList的多线程并发的安全性就被保证了,多线程并发写的时候

并发写CopyOnWriteArrayList的性能是较差的,基本上所有的线程都需要串行起来写CopyOnwriteArrayList,一个线程先写完,下一个线程才能写

csharp 复制代码
public E get(int index) {
    return get(getArray(), index);
}
css 复制代码
private E get(Object[] a, int index) {
    return (E) a[index];
}

写数据的时候一定要CopyOnWrite,如何解决读写并发的问题,写数据的时候,如何安全的读数据

CopyOnWriteArrayList设计思想,并不是基于CAS执行读写操作,写数据的时候,全部复制一个副本,新的数组,对新的数组来修改,修改好了设置回去就可以了。读数据,只有两种情况

第一种:我读到的老数组的数据

第二种:其他线程更新好了数组,volatile写,我读到的是新数组的数据

我不需要依赖任何一种加锁的机制来保证数据读写并发的安全性,我甚至都不需要依赖于Unsafe.getObjectVolatile(),volatile读机制,来读取数组里的元素,我直接就是最最简单,最最高效

最最高性能的,读就是直接读当前数组的数据即可,要么读的是老数据,要么读的是最新的数据,都有可能

写数据更新的是复制好的另外一个副本数组,同一时间大量的线程读数据的时候,都是在读老数组的数据,读写之间是没有任何的并发冲突问题的,读和写之间是没有锁的冲突的,写的是副本数组

CopyOnWrite机制,写副本数组,跟读就没关系了,只要写完成之后,走一个volatile写,设置最新的数组,自然读操作就会读到最新数组的元素了,只有一个线程可以写,但是写的同时可以允许大量的线程来并发读

vbnet 复制代码
List<String> list = new CopyOnWriteArrayList<String>();
list.add("张三");
list.set(0,"李四");
list.remove(0);
list.get(0);

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
    System.out.println(iterator.next());
}
csharp 复制代码
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
arduino 复制代码
static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        if (! hasPrevious())
            throw new NoSuchElementException();
        return (E) snapshot[--cursor];
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor-1;
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code remove}
     *         is not supported by this iterator.
     */
    public void remove() {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code set}
     *         is not supported by this iterator.
     */
    public void set(E e) {
        throw new UnsupportedOperationException();
    }

    /**
     * Not supported. Always throws UnsupportedOperationException.
     * @throws UnsupportedOperationException always; {@code add}
     *         is not supported by this iterator.
     */
    public void add(E e) {
        throw new UnsupportedOperationException();
    }

    @Override
    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        Object[] elements = snapshot;
        final int size = elements.length;
        for (int i = cursor; i < size; i++) {
            @SuppressWarnings("unchecked") E e = (E) elements[i];
            action.accept(e);
        }
        cursor = size;
    }
}

Iterator迭代器里面是包含了一个snapshot,指向的就是CopyOnWriteArrayList当前的数组,array对象,你创建了一个Iterator迭代器对象之后,在迭代的过程中,都是基于快照数组在进行遍历的

如果此时有另外一个线程更新了数组的元素,设置了array,但是此时对迭代是没有任何影响的,你的迭代器里有一个snapshot指针指向了老的数组对象,他会一直用这个老的数组完成遍历

迭代也是基于快照的机制来实现的,读操作其实跟写操作都是半毛钱关系没有的,CopyOnWrite机制,这样的一套设计思想,保证了就是说你的读的并发能力是很强的,不需要加锁的

CopyOnWriteArrayList:弱一致性

多个线程并发的读写list,中间一定是有一段时间,是复制数组被修改好了,还没设置给array;但是此时其他线程读到的都是老数组的数据,这个过程中,多个线程看到的数据是不一致的,人家修改了数据没有立马被人读到

弱一致性 -> 最终一致性

优点:读和写不互斥的,写和写互斥,同一时间就一个人可以写,但是写的同时可以允许其他所有人来读;读和读也是并发的;读写锁机制还要好;他也不涉及到Unsafe.getObjectVolatile

使用场景:多线程并发安全性,可以选用他;尽可能是读多写少的场景,大量的读是不被影响的;可能有一个线程刚刚发起了写,此时别的线程读到的还是旧的数据,也有这种可能,还好

缺点:空间换时间,写的时候,经常内存里会出现复制出来的一模一样的副本,对内存消耗过大,副本机制保证了保证读写并发优化,大量的并发读不需要锁互斥,list如果很大,可能你要考虑在线上运行的时候,可能经常

内存占用会是list大小的几倍

相关推荐
楚兴1 小时前
使用 Eino 和 Ollama 构建智能 Go 应用:从简单问答到复杂 Agent
人工智能·后端
小镇cxy1 小时前
VibeCoding实践,Spec+Claude Code小程序开发
后端·claude·vibecoding
GeekPMAlex1 小时前
深入理解 Python 元组、哈希、堆与 enumerate
后端
踏浪无痕1 小时前
从单体PHP到微服务:一个五年老项目的血泪重构史
后端·面试·架构
shark_chili1 小时前
基于arthas量化监控诊断java应用方法论与实践
后端
+VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue在线考试管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
okseekw2 小时前
Java泛型从入门到实战:原理、用法与案例深度解析
java·后端
雨中飘荡的记忆2 小时前
Spring WebFlux详解
java·后端·spring
文攀2 小时前
Go 语言 GMP 调度模型深度解析
后端·go·编程语言