深度解析CopyWriteArrayList工作原理

引言

在 Java 并发编程领域,对于线程安全的列表需求颇为常见。CopyOnWriteArrayList 作为 java.util.concurrent 包下的一员,为我们提供了一种独特的线程安全列表实现方式。它特别适用于读多写少的场景,能在保证线程安全的同时,尽量减少对读操作的性能影响。本文将深入探究 CopyOnWriteArrayList 的原理,涵盖其底层数据结构、核心属性、构造方法、常用操作的实现细节、性能特点以及应用场景。

1. CopyOnWriteArrayList 概述

1.1 定义与用途

CopyOnWriteArrayList 是 Java 中实现了 List 接口的线程安全列表。"CopyOnWrite" 即写时复制,这意味着在进行写操作(如添加、删除、修改元素)时,它会创建一个原数组的副本,在副本上进行修改,修改完成后再将引用指向新的数组。这种机制使得读操作无需加锁,从而提高了读操作的性能,尤其适用于读操作远远多于写操作的场景。

1.2 继承关系与实现接口

CopyOnWriteArrayList 实现了 List 接口,这表明它具备列表的基本特性,如可以按索引访问元素、支持元素的添加和删除等。同时,它还实现了 RandomAccess 接口,支持随机访问,以及 Cloneablejava.io.Serializable 接口,允许进行克隆和序列化操作。

java 复制代码
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListOverview {
    public static void main(String[] args) {
        // 创建一个 CopyOnWriteArrayList 对象
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        // 可以将其赋值给 List 接口类型的变量
        List<String> listInterface = list;
    }
}

2. 底层数据结构:可复制的数组

2.1 基本概念

CopyOnWriteArrayList 底层基于数组实现。与普通数组不同的是,在进行写操作时,它会创建一个新的数组副本,将原数组中的元素复制到新数组中,然后在新数组上进行修改,最后将引用指向新数组。这样,读操作可以在原数组上进行,无需加锁,从而提高了读操作的并发性能。

2.2 核心属性

CopyOnWriteArrayList 有一个核心属性用于存储数组:

java 复制代码
private transient volatile Object[] array;
  • array:用于存储列表中的元素。volatile 关键字保证了该数组的可见性,即一个线程对数组的修改能够及时被其他线程看到。

3. 构造方法

3.1 无参构造方法

java 复制代码
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

无参构造方法创建一个空的 CopyOnWriteArrayList,初始数组为空。

3.2 带集合参数的构造方法

java 复制代码
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        elements = c.toArray();
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elements.getClass() != Object[].class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

该构造方法接受一个集合作为参数,将集合中的元素复制到 CopyOnWriteArrayList 中。如果传入的集合也是 CopyOnWriteArrayList 类型,则直接获取其底层数组;否则,将集合转换为数组并进行复制。

3.3 带数组参数的构造方法

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

此构造方法接受一个数组作为参数,将数组中的元素复制到 CopyOnWriteArrayList 中。

java 复制代码
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListConstructors {
    public static void main(String[] args) {
        // 无参构造方法
        CopyOnWriteArrayList<String> list1 = new CopyOnWriteArrayList<>();

        // 带集合参数的构造方法
        Collection<String> collection = new ArrayList<>();
        collection.add("apple");
        collection.add("banana");
        CopyOnWriteArrayList<String> list2 = new CopyOnWriteArrayList<>(collection);

        // 带数组参数的构造方法
        String[] array = {"cherry", "date"};
        CopyOnWriteArrayList<String> list3 = new CopyOnWriteArrayList<>(array);

        System.out.println(list2);
        System.out.println(list3);
    }
}

4. 常用操作原理

4.1 添加元素

java 复制代码
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();
    }
}
  • add(E e) 方法使用 ReentrantLock 进行加锁,确保写操作的线程安全。
  • 首先获取原数组,然后创建一个长度比原数组大 1 的新数组。
  • 将原数组中的元素复制到新数组中,并将新元素添加到新数组的末尾。
  • 最后将引用指向新数组,并释放锁。

4.2 访问元素

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

private E get(Object[] a, int index) {
    return (E) a[index];
}
  • get(int index) 方法直接从数组中获取指定索引的元素,无需加锁。这是因为读操作是在原数组上进行的,不会受到写操作创建新数组的影响。

4.3 修改元素

java 复制代码
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();
    }
}
  • set(int index, E element) 方法使用 ReentrantLock 进行加锁。
  • 首先获取原数组,并获取指定索引的旧元素。
  • 如果新元素与旧元素不同,则创建一个新数组,将原数组中的元素复制到新数组中,并修改指定索引的元素。
  • 最后将引用指向新数组,并释放锁。

4.4 删除元素

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)
            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(int index) 方法使用 ReentrantLock 进行加锁。
  • 首先获取原数组,并获取指定索引的元素。
  • 如果删除的是数组的最后一个元素,则直接创建一个长度比原数组小 1 的新数组。
  • 否则,创建一个新数组,将原数组中指定索引之前和之后的元素复制到新数组中。
  • 最后将引用指向新数组,并释放锁。

5. 迭代器原理

CopyOnWriteArrayList 的迭代器是弱一致性的。当创建迭代器时,它会保存当前数组的快照,迭代器的操作是基于这个快照进行的,不会受到后续写操作的影响。

java 复制代码
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListIterator {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("apple");
        list.add("banana");

        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        // 在迭代过程中进行写操作
        list.add("cherry");

        // 再次迭代,使用新的迭代器
        Iterator<String> newIterator = list.iterator();
        while (newIterator.hasNext()) {
            System.out.println(newIterator.next());
        }
    }
}

6. 性能分析

6.1 时间复杂度

  • 读操作 :读操作(如 get 方法)的时间复杂度为 O(1),因为直接从数组中获取元素,无需加锁。
  • 写操作 :写操作(如 addsetremove 方法)的时间复杂度为 O(n),因为需要创建新数组并复制元素。

6.2 空间复杂度

CopyOnWriteArrayList 的空间复杂度较高,因为每次写操作都会创建一个新数组,会占用额外的内存空间。

7. 应用场景

7.1 读多写少的场景

由于 CopyOnWriteArrayList 的读操作无需加锁,性能较高,因此适用于读操作远远多于写操作的场景,如配置信息存储、缓存等。

7.2 对数据实时性要求不高的场景

由于迭代器是弱一致性的,在迭代过程中可能看不到最新的数据更新,因此适用于对数据实时性要求不高的场景。

8. 总结

CopyOnWriteArrayList 通过写时复制的机制,在保证线程安全的同时,提高了读操作的并发性能。它适用于读多写少、对数据实时性要求不高的场景。然而,由于每次写操作都会创建新数组,会带来较高的空间开销和写操作性能开销。在使用时,需要根据具体的业务场景权衡其优缺点,合理选择数据结构。

相关推荐
tangweiguo030519874 分钟前
Kotlin实现Android应用保活方案
android·kotlin
大胃粥17 分钟前
Android V app 冷启动(9) Activity 生命周期调度
android
洛小豆24 分钟前
一个场景搞明白Reachability Fence,它就像一道“结账前别走”的红外感应门
java·后端·面试
500佰26 分钟前
AI提示词(Prompt)设计优化方案 | 高效使用 AI 工具
java·人工智能·prompt·ai编程
摘星编程28 分钟前
并发设计模式实战系列(4):线程池
java·设计模式·并发编程
IT专家-大狗33 分钟前
Edge浏览器安卓版流畅度与广告拦截功能评测【不卡还净】
android·前端·edge
一笑的小酒馆36 分钟前
AndroidRom定制删除Settings某些菜单选项
android
PGCCC41 分钟前
【PGCCC】Postgres MVCC 内部:更新与插入的隐性成本
java·开发语言·数据库
诺亚凹凸曼1 小时前
Java基础系列-LinkedList源码解析
java·开发语言
火柴就是我1 小时前
android drawText 绘制 数字 注意点
android