CopyOnWriteArrayList源码解析

Java并发包提供了很多线程安全的集合,有了他们的存在,使得我们在多线程开发下,可以和单线程一样去编写代码,大大简化了多线程开发的难度,但在实际操作过程中,因为我们并不理解其中的一些隐藏陷阱,可能导致我们在开发的时候频频出现问题,今天针对这篇源码解析,我们来聊一聊CopyOnWriteArrayList隐藏的危机。

其实看到我们这里我们不难看出,从字面上意思来说就是在写操作的时候进行赋值的列表集合,真的是这样的吗,让我们扒开他其中的一个方法的源码简单的来分析一下他的核心思想:

  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();
        }
    }

这个是CopyOnWriteArrayList里面的一个add()方法,从代码可以看出他调用这个方法的时候,先获取一把独占锁,保证在进行添加方法的时候其他线程并不能对其进行操作,保证了线程安全。之后获取数组,接下来进入到最关键的一步-------创建一个新数组并把复制出新的数组,然后再进行数组长度的修改,并替换到旧的数组,此时便完成了一个添加的操作,这样做的目的其实是在不改变原数组的结构上进行复制与修改,防止出现多线程的情况下,本身结构进行改变。
remove
我们可以通过remove删除指定坐标的元素。

 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();
        }
    }

可以看到,remove方法和add,set方法是一样的,第一步还是先获取独占锁,来保证线程安全性,如果要删除的元素是最后一个,则复制出一个长度为【旧数组的长度-1】的新数组,随之替换,这样就巧妙的把最后一个元素给删除了,如果要删除的元素不是最后一个,则分两次复制,随之替换。
迭代器

 public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
  static final class COWIterator<E> implements ListIterator<E> {
    
        private final Object[] snapshot;
     
        private int cursor;

        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
        
        // 判断是否还有下一个元素
        public boolean hasNext() {
            return cursor < snapshot.length;
        }
        
        //获取下个元素
        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

代码也比较简单,通俗易懂就不再解释了,在这里我们可以看到当我们调用iterator方法获取迭代器,内部会调用COWIterator的构造方法,此构造方法有两个参数,第一个参数就是array数组,第二个参数是下标,就是0。随后构造方法中会把array数组赋值给snapshot变量。
snapshot是“快照”的意思,如果Java基础尚可的话,应该知道数组是引用类型,传递的是指针,如果有其他地方修改了数组,这里应该马上就可以反应出来,那为什么又会是snapshot这样的命名呢?没错,如果其他线程没有对CopyOnWriteArrayList进行增删改的操作,那么snapshot就是本身的array,但是如果其他线程对CopyOnWriteArrayList进行了增删改的操作,旧的数组会被新的数组给替换掉,但是snapshot还是原来旧的数组的引用。也就是说 当我们使用迭代器便利CopyOnWriteArrayList的时候,不能保证拿到的数据是最新的,这也是弱一致性问题。让我们通过下面简单的代码进行验证:

public class Solution{
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2020");
        copyOnWriteArrayList.add("zs");
        copyOnWriteArrayList.add("zss");
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

}

在这里插入图片描述

通过结果我们可以看出,操作之间睡眠几秒就是为了保证线程运行完毕,我们可以看到这就是我们想要的结果,但如果不让他睡眠,直接进入通过运行会出现什么情况呢?

public class Solution{
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList=new CopyOnWriteArrayList<>();
        copyOnWriteArrayList.add("Hello");
        copyOnWriteArrayList.add("CopyOnWriteArrayList");
        copyOnWriteArrayList.add("2020");
        copyOnWriteArrayList.add("zs");
        copyOnWriteArrayList.add("zss");
        new Thread(()->{
            copyOnWriteArrayList.remove(1);
            copyOnWriteArrayList.remove(3);
        }).start();
//        try {
//            TimeUnit.SECONDS.sleep(3);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        Iterator<String> iterator = copyOnWriteArrayList.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

}

此时我们将睡眠状态去掉,我们可能会想到上面线程不会执行完成就会进行遍历了,有可能正在进行各种操作呢。。。没错,就是这种情况
在这里插入图片描述
我们可以看到,结果确实和我们预想的一样,因为在执行操作的时候,具有弱一致性,所以无法第一时间保证数据的一致性,这个便是我们在开发中CopyOnWriteArrayList在多线程的环境下让我们头疼的问题,希望看完这篇文章可以对你的debug有帮助呦!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值