Java并发编程—CopyOnWriteArrayList

本文介绍了Java并发编程中的CopyOnWriteArrayList,它是一个线程安全的列表,采用写时复制策略。文章详细讲解了CopyOnWriteArrayList的构造方法、添加元素、获取指定位置元素、修改元素、删除元素以及迭代器遍历的操作,并指出在并发环境下可能出现的弱一致性问题。

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

1 概述

JUC并发包中只提供了一个并发list就是CopyOnWriteList,是线程安全的(内部有一个独占锁ReetrantLock),采用的是写时复制的策略。整体的类图结构如下图所示。

下面从构造方法,增删改查与迭代器遍历来对该类进行一些详细的介绍。

2 构造方法

该类共有三个构造方法,一个无参构造,一个传入一个集合的有参构造,一个是传入一个泛型数组的有参构造

public CopyOnWriteArrayList() //无参构造器在内部创建了一个大小为0的Object数组
public CopyOnWriteArrayList(Collection<? extends E> c) //传入一个集合,将集合里的元素赋值到本list
public CopyOnWriteArrayList(E[] toCopyIn) //创建一个list其内部元素时传入toCopyIn的副本

3 添加元素

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;
                //更新当前list的Object[]
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();//释放锁资源
            }
        }

 该类中还有其他的添加元素的方法,比如add(int index, E element), addIfAbsent(E e)等,其原理与上面的add(E e)方法基本一致

4 获取指定位置的元素

主要是指该类的get()方法,这时共设计到三个方法的调用

        //获得指定位置的元素的位置
        public E get(int index) {
            return get(getArray(), index);
        }
        //获得内部的数组
        final Object[] getArray() {
            return array;
        }
        //此get方法传入一个数组与索引后尝试获取传入数组index位置的元素
        private E get(Object[] a, int index) {
            return (E) a[index];
        }

 可以看到获取指定位置的元素的过程并没有加锁,所以该过程并不是线程安全的,也就存在所谓的弱一致性问题,如下图

 线程a调用get(0)来获得List索引为0位置上的元素,前面看源码部分可以看到一共有三步,当执行完第二步执行最后一步get(Object[] a, int index)之前线程B对List执行了add(0, 4)操作,即此时List中的array索引0位置上的元素发生改变,虽然该数组是volatile的,线程A能够马上知道,但是此时线程执行查询最后一步时传入的Object[]数组a(也就是图中的tempArray)还是原来的数组,此时就是用原来的数组进行查询,返回的是1而不是4,当查询结束后tempArray就没有其他引用指向他了,也就成为了垃圾等待GC回收。

5 修改指定元素

set(int index, E element)方法,可以看到也是采用了写时复制策略

public E set(int index, E element) {
            //获取独占锁
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                //获取当前的array
                Object[] elements = getArray();
                //获取当前array中index位置处的元素
                E oldValue = get(elements, index);
                //当原来的元素与要重新设置的元素不一致时进入修改的逻辑
                if (oldValue != element) {
                    //复制一份当前的数组后将相应位置的元素修改为element后再将其设置为当前的array
                    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();
            }
        }

6 删除元素

这里我们主要来看一下remove(int index)方法,其余删除元素的方法的原理与其基本一致

public E remove(int index) {
            //获取独占锁
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                //获取当前的array
                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();
            }
        }

7 迭代器遍历

//这是List中获取迭代器的方法是根据当前的array创建了一个COWIterator(这是CopyOnWriteArrayList中的一个内部类)
public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

获取迭代器后的遍历操作就和普通的集合一样,这里就不再做介绍,需要注意的是,该迭代器也存在弱一致性的问题,因为我们可以看到获取迭代器会传入当前的array,若在当前线程使用迭代器进行遍历前有其他线程对List进行了增删改的操作,那么迭代的结果仍然是原来的array,原因与上面讲到get()方法时是一样的。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值