JUC-CopyOnWriteArrayList

jdk版本: 1.8

更多数据结构,算法,设计模式,源码分析等请关注我的微信公众号[技术寨],每周至少两篇优质文章

1.设计思想

正常情况下,我们要设计一个线程安全的ArrayList,肯定最终会设计成Vector一样的类,最多只是将synchronized关键字改为lock的方式。但是这种设计带来了两个明显的弊端:

  • 读取数据必须加锁
    因为修改的是同一个数组,所以如果读取数据时不加锁,可能会读取到修改操作的中间状态。比如:往中间添加元素时,刚移动元素。
    添加元素中间状态
    这样获取到的数据肯定是错误的。
  • 存在ConcurrentModificationException的风险
    迭代器,SubList等内部类,访问的是同一个数组,所以存在修改之后数据不一致的问题,从而出现ConcurrentModificationException。

解决方式

我们会发现,上面两个问题其实都是一个原因导致的,就是所有的操作都在同一个数组上,导致修改时会相互影响。所以是不是可以每次修改操作都创建一个新的数组,这样每次生成一个不变的数组,就可以避免这个问题。
但是这种方式也会存在一些缺点,大家先想想,我在后面会总结一下。

1.基本概述

1.1 继承关系

继承关系
和ArrayList一样,它实现了Cloneable,Serializable,RandomAccess接口,说明它分别具有复制,序列化和快速随机访问的能力。

1.2 主要属性

private transient volatile Object[] array;

这里最关键的修饰符是volatile,前面已经介绍到了这个关键字。它本身具有可见性有序性,如果只是简单的将该变量进行赋值操作,那它就相当于是原子操作,即具备原子性,所以volatile的赋值操作是多线程安全的。

1.3 主要方法

方法
通过这张图可以看出,其大部分的方法都是实现自List,在[ArrayList图解]这篇文章中我们已经介绍地七七八八了,所以方法的基本介绍就直接跳过了,我们直接图解核心方法。

2. 核心方法图解

2.1 添加元素

    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;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

添加元素就可以看出CopyOnWriteArrayList的设计思想,在新数组上执行修改操作。
添加元素

2.2 获取元素

 public E get(int index) {
        return get(getArray(), index);
 }

这里获取下标为index的元素代码非常简单,并且不用加锁。

2.3 删除元素

    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)
                // 此时index=len-1,其实就是删除最后一个元素
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                // 删除中间元素,需要将数组拆分成[0,index)和[index+1,len-1)这两个部分
                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();
        }
    }

修改操作比较简单,直接看动图就知道执行过程。(下图只展示index在中间位置的情况)
删除元素

2.4 特殊方法-不存在添加addIfAbsent

    // false: 已经存在元素e或者添加失败, true: 添加成功
    public boolean addIfAbsent(E e) {
        //先获取快照
        Object[] snapshot = getArray();
        // 如果indexOf结果等于0说明已经存在该元素,直接返回false
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

    private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            if (snapshot != current) {
                // 如果在进入锁之前有修改操作,snapshot != current
                int common = Math.min(snapshot.length, len);
                for (int i = 0; i < common; i++)
                    // 因为已经判断了snapshot,所以如果current[i] == snapshot[i],肯定!eq(e, current[i])
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                // 检查当前数组中是不是存在元素e
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            // 如果不存在,就添加元素e
            Object[] newElements = Arrays.copyOf(current, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

这里为了性能优化,判断indexOf(e, snapshot, 0, snapshot.length)>=0,就直接返回操作失败。其实这里有一个小的细节需要说明,如果在刚通过Object[] snapshot = getArray()获取数组快照时,如果刚好删除了元素e操作并生成了新的数组,这时理论上应该添加元素。但是这里为什么不考虑这种情况呢?我认为这个需要从这个方法的目的来说,这个方法最根本的目的是为了防止添加重复元素,为了更好的性能,可以适当牺牲一点。

3.简单的迭代器

通过CopyOnWriteArrayList.iterator方法可以获取它的迭代器对象,因为存在不变的数组,所以迭代器实现根本不用考虑线程安全的问题,所以实现非常简单,这里我们把它核心方法展示一下:


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

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

看!根本不用考虑线程安全和ConcurrentModificationException的问题,只需检查指针位置就完了。

4.优点和缺点

4.1 优点

(1)线程安全
(2)读性能非常好,非常适合多读少写的场景

4.2 缺点

(1)因为每次修改都需要创建新的数组,所以内存消耗稍大
(2)读操作都是针对当时的快照,所以存在数据不一致的问题。

5.面试必知

5.1 CopyOnWriteArrayList是如何扩容的

CopyOnWriteArrayList每次添加都是重新创建一个新的数组,所以并不需要一些预测性的算法计算扩容之后的大小,只需将原数组长度+1即可。

5.2 CopyOnWriteArraySet实现方法

CopyOnWriteArraySet的底层就是调用CopyOnWriteArrayList的方法,只不过在添加时为了保证集合元素唯一使用的是CopyOnWriteArrayList.addIfAbsent方法

关注我的公众号,不迷路

公众号

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值