前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java并发——CopyOnArrayList

Java并发——CopyOnArrayList

原创
作者头像
翰墨飘香
发布2024-04-20 18:52:27
830
发布2024-04-20 18:52:27
举报
文章被收录于专栏:Java并发Java并发

一、CopyOnArrayList概述

1.1 概述

ArrayList是线程不安全的集合,而CopyOnWriteArrayList是一种线程安全的ArrayList,底层是基于数组实现,不过该数组使用了volatile关键字修饰。

实现线程安全的原理是,“人如其名”,就是 Copy On Write(写时复制),意思就是在对其进行修改操作的时候,复制一个新的ArrayList,在新的ArrayList上进行修改操作,从而不影响旧的ArrayList的读操作,完成修改之后,再将原容器的引用指向新的容器。

1.2 优点

线程安全

通过volatile关键字 + 锁 + 数组复制实现的线程安全

CopyOnWriteArrayList 利用了“不变性”原理,因为容器每次修改都是创建新副本,所以对于旧容器来说,其实是不可变的,也是线程安全的,无需进一步的同步操作。我们可以对 CopyOnWrite 容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,也不会有修改。

读写分离

CopyOnWriteArrayList 的所有修改操作(add,set等)都是通过创建底层数组的新副本来实现的,所以 CopyOnWrite 容器也是一种读写分离的思想体现,读和写使用不同的容

迭代期间允许修改集合内容

CopyOnWriteArrayList 的迭代器在迭代的时候,如果数组内容被修改了,CopyOnWriteArrayList 不会报 ConcurrentModificationException 的异常,因为迭代器使用的依然是旧数组,只不过迭代的内容可能已经过时了

1.3 缺点

内存占用问题

因为 CopyOnWrite 的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,这一点会占用额外的内存空间

在元素较多或者复杂的情况下,复制的开销很大

复制过程不仅会占用双倍内存,还需要消耗 CPU 等资源,会降低整体性能

数据一致性问题

数据不是强一致的,是弱一致的

由于 CopyOnWrite 容器的修改是先修改副本,所以这次修改对于其他线程来说,并不是实时能看到的,只有在修改完之后才能体现出来。如果你希望写入的的数据马上能被其他线程看到,CopyOnWrite 容器是不适用的

二、适用场景

读操作可以尽可能的快,而写即使慢一些也没关系

在很多应用场景中,读操作可能会远远多于写操作。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁的访问。对于这种场景,我们最希望看到的就是读操作可以尽可能的快,而写即使慢一些也没关系。

再例如,新闻的场景

适合读多写少的场景

黑名单是最典型的场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单中,黑名单并不需要实时更新,可能每天晚上更新一次就可以了。当用户搜索时,会检查当前关键字在不在黑名单中,如果在,则提示不能搜索。这种读多写少的场景也很适合使用 CopyOnWrite 集合。

三、CopyOnArrayList原理

使用ReentrantLock加锁,保证操作过程中线程安全。

使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到

四、源码分析

数据结构

代码语言:java
复制
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** CopyOnWriteArrayList底层由数组实现,volatile修饰,保证数组的可见性 */
    private transient volatile Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }
    //初始化CopyOnWriteArrayList相当于初始化数组
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

添加元素 add

添加元素的流程:

①先使用ReentrantLock加锁,保证线程安全。

②再创建一个新数组,长度是原数组长度+1,并把原数组元素拷贝到新数组里面。

③然后在新数组末尾位置赋值

④使用新数组替换掉原数组

⑤最后释放锁

代码语言:java
复制
    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    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();
        }
    }

get

get 相关的操作没有加锁,保证了读取操作的高速

代码语言:java
复制
public E get(int index) {
    return get(getArray(), index);
}

final Object[] getArray() {
    return array;
}

private E get(Object[] a, int index) {
    return (E) a[index];
}

set

代码语言:java
复制
/**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
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();
        }
    }

remove删除元素

删除元素的流程:

①先使用ReentrantLock加锁,保证线程安全。

②再创建一个新数组,长度是原数组长度-1,并把原数组中剩余元素(不包含需要删除的元素)拷贝到新数组里面。

③使用新数组替换掉原数组

④最后释放锁

代码语言:java
复制
// 按照下标删除元素
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) {
            // 0表示删除的是数组末尾的元素
            setArray(Arrays.copyOf(elements, len - 1));
        } else {
            // 创建一个新数组,长度是原数组长度-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();
    }
}

迭代器

迭代器有两个重要的属性,分别是 Object[] snapshot 和 int cursor。

其中 snapshot 代表数组的快照,也就是创建迭代器那个时刻的数组情况,而 cursor 则是迭代器的游标。

迭代器在被构建的时候,会把当时的 elements 赋值给 snapshot,而之后的迭代器所有的操作都基于 snapshot 数组进行的

在 next 方法中可以看到,返回的内容是 snapshot 对象,所以,后续就算原数组被修改,这个 snapshot 既不会感知到,也不会受影响,执行迭代操作不需要加锁,也不会因此抛出异常。迭代器返回的结果,和创建迭代器的时候的内容一致

代码语言:java
复制
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++];
        }

五、Demo

代码语言:java
复制
/**
* 描述: 演示CopyOnWriteArrayList迭代期间可以修改集合的内容
*/

public class CopyOnWriteArrayListDemo {

    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(new Integer[]{1, 2, 3});
        System.out.println(list); //[1, 2, 3]

        //Get iterator 1
        Iterator<Integer> itr1 = list.iterator();
        //Add one element and verify list is updated
        list.add(4);
        System.out.println(list); //[1, 2, 3, 4]
        //Get iterator 2
        Iterator<Integer> itr2 = list.iterator();
        System.out.println("====Verify Iterator 1 content====");
        itr1.forEachRemaining(System.out::println); //1,2,3
        System.out.println("====Verify Iterator 2 content====");
        itr2.forEachRemaining(System.out::println); //1,2,3,4

    }

}
代码语言:java
复制
[1, 2, 3]

[1, 2, 3, 4]

====Verify Iterator 1 content====

1

2

3

====Verify Iterator 2 content====

1

2

3

4

这两个迭代器打印出来的内容是不一样的。第一个迭代器打印出来的是 1, 2, 3,而第二个打印出来的是 1, 2, 3, 4。虽然它们的打印时机都发生在第四个元素被添加之后,但它们的创建时机是不同的。由于迭代器 1 被创建时的 List 里面只有三个元素,后续无论 List 有什么修改,对它来说都是无感知的

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、CopyOnArrayList概述
    • 1.1 概述
      • 1.2 优点
        • 1.3 缺点
        • 二、适用场景
          • 读操作可以尽可能的快,而写即使慢一些也没关系
            • 适合读多写少的场景
            • 三、CopyOnArrayList原理
            • 四、源码分析
              • 数据结构
                • 添加元素 add
                  • get
                    • set
                      • remove删除元素
                        • 迭代器
                        • 五、Demo
                        相关产品与服务
                        容器服务
                        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                        http://www.vxiaotou.com