• CopyOnWriteArrayList是如何保证线程安全的?


    一:前言

    在我们需要保证线程安全的时候,如果使用到Map,那么我们可以使用线程安全的ConcurrentHashMapConcurrentHashMap不仅可以保证线程安全,而且效率也非常不错,那有没有线程安全的List呢?答案是有,那就是CopyOnWriteArrayList。今天我们就一起来了解一下CopyOnWriteArrayList,看它是如何巧妙的保证线程安全的吧。

    二:成员变量分析

    1. //进行修改操作时的锁
    2. final transient ReentrantLock lock = new ReentrantLock();
    3. //真正保存数据的数组 用volatile关键字进行修饰,保证array的引用的可见性
    4. private transient volatile Object[] array;
    5. 复制代码

    三:源码分析

    首先我们看构造方法,CopyOnWriteArrayList有三个构造方法。

    1.空参构造

    调用setArray方法将成员变量array赋值为一个长度为0的数组。

    1. public CopyOnWriteArrayList() {
    2. setArray(new Object[0]);
    3. }
    4. 复制代码
    1. final void setArray(Object[] a) {
    2. array = a;
    3. }
    4. 复制代码

    2.传入一个Collection对象的构造方法

    首先判断Collection是否是一个CopyOnWriteArrayList,如果是,直接将传入的CopyOnWriteArrayList的elements重新赋值给需要创建的CopyOnWriteArrayList。 如果不是,判断Collection是否是ArrayList,如果是,那么就利用toArray()方法将其转化为一个数组并赋值给成员变量array,否则将Collection里面的元素全部取出来copy到一个新数组中,并且将该数组赋值给成员变量array。

    1. public CopyOnWriteArrayList(Collection<? extends E> c) {
    2. Object[] elements;
    3. if (c.getClass() == CopyOnWriteArrayList.class)
    4. elements = ((CopyOnWriteArrayList<?>)c).getArray();
    5. else {
    6. elements = c.toArray();
    7. if (c.getClass() != ArrayList.class)
    8. elements = Arrays.copyOf(elements, elements.length, Object[].class);
    9. }
    10. setArray(elements);
    11. }
    12. 复制代码

    3.传入一个数组的构造方法

    将传入的数组的元素copy到一个新的Object数组,并且赋值给成员变量array。

    1. public CopyOnWriteArrayList(E[] toCopyIn) {
    2. setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    3. }
    4. 复制代码

    接下来我们看核心的add(),remove(),get()方法。

    add(E e)

    首先加锁,然后通过Arrays.copyOf()方法将元素copy到一个新的数组中,新的数组的长度为原数组的长度+1,并且将需要加入的元素赋值到新数组的最后。最后将新数组赋值给成员变量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. Object[] newElements = Arrays.copyOf(elements, len + 1);
    8. newElements[len] = e;
    9. setArray(newElements);
    10. return true;
    11. } finally {
    12. lock.unlock();
    13. }
    14. }
    15. 复制代码

    add(int index, E element)

    add(int index, E element)方法需要将元素加入到指定的索引位置中。首先也是先加锁,保证线程安全,将原数组分为两段进行操作,根据index进行分隔,分别copy index之前的元素和之后的元素,copy完成之后在将需要插入的元素设置到索引为index的位置上。之后将新数组赋值给成员变量array。

    1. public void add(int index, E element) {
    2. final ReentrantLock lock = this.lock;
    3. lock.lock();
    4. try {
    5. Object[] elements = getArray();
    6. int len = elements.length;
    7. if (index > len || index < 0)
    8. throw new IndexOutOfBoundsException("Index: "+index+
    9. ", Size: "+len);
    10. Object[] newElements;
    11. int numMoved = len - index;
    12. if (numMoved == 0)
    13. newElements = Arrays.copyOf(elements, len + 1);
    14. else {
    15. newElements = new Object[len + 1];
    16. System.arraycopy(elements, 0, newElements, 0, index);
    17. System.arraycopy(elements, index, newElements, index + 1,
    18. numMoved);
    19. }
    20. newElements[index] = element;
    21. setArray(newElements);
    22. } finally {
    23. lock.unlock();
    24. }
    25. }
    26. 复制代码

    接下来看remove()方法。

    remove(int index)

    remove(int index)方法需要在数组中移除指定索引的值。首先是加锁,同样也是将原数组分为两段进行操作,根据index进行分隔,分别copy index之前的元素和之后的元素,copy到一个新数组中,新数组的长度为原数组的长度减一(注意这里是没有copy index索引位置的值的,所以相当于移除了index索引上的值)。之后将新数组赋值给成员变量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. E oldValue = get(elements, index);
    8. int numMoved = len - index - 1;
    9. if (numMoved == 0)
    10. setArray(Arrays.copyOf(elements, len - 1));
    11. else {
    12. Object[] newElements = new Object[len - 1];
    13. System.arraycopy(elements, 0, newElements, 0, index);
    14. System.arraycopy(elements, index + 1, newElements, index,
    15. numMoved);
    16. setArray(newElements);
    17. }
    18. return oldValue;
    19. } finally {
    20. lock.unlock();
    21. }
    22. }
    23. 复制代码

    接下来看get()方法

    get()

    我们可以看到get()方法很简单,就是从array成员变量中取出对应索引的值。并没有加锁处理。所以尽管是在并发高的情况下,get()方法的效率依旧是比较高的。

    1. /**
    2. * {@inheritDoc}
    3. *
    4. * @throws IndexOutOfBoundsException {@inheritDoc}
    5. */
    6. public E get(int index) {
    7. return get(getArray(), index);
    8. }
    9. private E get(Object[] a, int index) {
    10. return (E) a[index];
    11. }
    12. 复制代码

    四:总结

    CopyOnWriteArrayList为什么能够保证线程安全,主要是因为以下几点:

    1.在做修改操作的时候加锁

    2.每次修改都是将元素copy到一个新的数组中,并且将数组赋值到成员变量array中。

    3.利用volatile关键字修饰成员变量array,这样就可以保证array的引用的可见性,每次修改之前都能够拿到最新的array引用。这点很关键。

    看到这里,相信你已经对CopyOnWriteArrayList非常了解了,CopyOnWriteArrayList在查询多,修改操作少的情况下效率是非常可观的,既能够保证线程安全,又能有不错的效率。但是如果修改操作较多,就会导致数组频繁的copy,效率就会有所下降,如果修改操作很多,那么直接使用Collections.synchronizedList(),或许也是一个不错的选择。

  • 相关阅读:
    运维少背锅? 7 种常见监控工具,你是否用过?
    Prim算法
    微信支付-商户平台
    Spring 面试题及答案整理,最新面试题
    kafka学习1 - 线程、进程消息通信方式、JMS模型、Kafka原理图
    【AIGC核心技术剖析】用于高效 3D 内容创建生成(从单视图图像生成高质量的纹理网格)
    勒索病毒该如何进行防范
    Flutter高仿微信-第35篇-单聊-视频通话
    贪心算法(二) | 重叠区间问题 | leecode刷题笔记
    可能是入门高阶数学的好通道 —— 很直观易记,又很难判断的真假的数学命题们
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/126869669