并发编程原理与实战(四十)深入理解CopyOnWriteArrayList:核心API与线程安全设计

HashMap对应的线程安全类ConcurrentHashMap,本文来学习ArrayList对应的线程安全类CopyOnwriteArrayList。

认识CopyOnWriteArrayList

CopyOnWriteArrayList是java.util.concurrent中的一个线程安全的List实现。它使用“写时复制”(Copy-On-Write)的策略,即在修改操作(如添加、设置、删除元素)时,复制一份当前数组进行修改,而不是直接在原数组上修改。这样可以保证读操作无需加锁,且不会读取到正在修改的数组,从而实现了线程安全。

/**
 * A thread-safe variant of {@link java.util.ArrayList} in which all mutative
 * operations ({@code add}, {@code set}, and so on) are implemented by
 * making a fresh copy of the underlying array.
 *
 * <p>This is ordinarily too costly, but may be <em>more</em> efficient
 * than alternatives when traversal operations vastly outnumber
 * mutations, and is useful when you cannot or don't want to
 * synchronize traversals, yet need to preclude interference among
 * concurrent threads.  The "snapshot" style iterator method uses a
 * reference to the state of the array at the point that the iterator
 * was created. This array never changes during the lifetime of the
 * iterator, so interference is impossible and the iterator is
 * guaranteed not to throw {@code ConcurrentModificationException}.
 * The iterator will not reflect additions, removals, or changes to
 * the list since the iterator was created.  Element-changing
 * operations on iterators themselves ({@code remove}, {@code set}, and
 * {@code add}) are not supported. These methods throw
 * {@code UnsupportedOperationException}.
 *
 * <p>All elements are permitted, including {@code null}.
 *
 * <p>Memory consistency effects: As with other concurrent
 * collections, actions in a thread prior to placing an object into a
 * {@code CopyOnWriteArrayList}
 * <a href=" "><i>happen-before</i></a >
 * actions subsequent to the access or removal of that element from
 * the {@code CopyOnWriteArrayList} in another thread.
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/java.base/java/util/package-summary.html#CollectionsFramework">
 * Java Collections Framework</a >.
 *
 * @since 1.5
 * @author Doug Lea
 * @param <E> the type of elements held in this list
 */
public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    //......
}

CopyOnWriteArrayList是一种线程安全的java.util.ArrayList 变体,其中所有的修改操作(add、set等)都是通过创建底层数组的一个新副本来实现的。

通常这种做法代价较高,但当遍历操作远多于修改操作时,可能比其他替代方案更高效。当你无法或不想对遍历操作进行同步,但又需要防止并发线程之间的干扰时,这种实现非常有用。“快照”风格的迭代器方法使用在创建迭代器时数组状态的引用。在迭代器的生命周期内,这个数组永远不会改变,因此不可能发生干扰,且保证迭代器不会抛出 ConcurrentModificationException。迭代器不会反映自创建以来对列表进行的添加、删除或更改操作。不支持对迭代器本身进行更改元素的操作(remove、set和add)。这些方法会抛出UnsupportedOperationException。

允许所有元素,包括null。

内存一致性影响:与其他并发集合一样,在一个线程将对象存入CopyOnWriteArrayList之前的操作,先发生于(happen-before)另一个线程从CopyOnWriteArrayList 中访问或删除该元素之后的操作。

CopyOnWriteArrayList核心API

构造函数

在这里插入图片描述

(1)无参构造函数

......
    
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

......
    
/**
 * Creates an empty list.
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
/**
 * Sets the array.
 */
final void setArray(Object[] a) {
    array = a;
}

创建一个空的 CopyOnWriteArrayList。从源码可以看出,setArray 方法用于设置底层存储数据的数组,这里初始化为一个长度为 0 的 Object 数组。

(2)参数为集合的构造函数

/**
 * Creates a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection of initially held elements
 * @throws NullPointerException if the specified collection is null
 */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] es;
    if (c.getClass() == CopyOnWriteArrayList.class)
        es = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        es = c.toArray();
        if (c.getClass() != java.util.ArrayList.class)
            es = Arrays.copyOf(es, es.length, Object[].class);
    }
    setArray(es);
}

使用给定集合中的元素初始化 CopyOnWriteArrayList。如果给定的集合本身就是 CopyOnWriteArrayList 类型,则直接复制其内部数组。否则,调用集合的 toArray 方法将其元素复制到一个新的数组中。
如果 toArray 返回的数组类型不是 Object[],则使用 Arrays.copyOf 方法将其复制到一个新的 Object[] 数组中。最后,调用 setArray 方法将新创建的数组设置为内部存储数组。

(3)参数为数组的构造函数

/**
 * Creates a list holding a copy of the given array.
 *
 * @param toCopyIn the array (a copy of this array is used as the
 *        internal array)
 * @throws NullPointerException if the specified array is null
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

使用给定数组中的元素初始化 CopyOnWriteArrayList,通过复制数组来保证原始数组不会被修改。使用 Arrays.copyOf 方法将给定的数组复制到一个新的 Object[] 数组中。调用 setArray 方法将新创建的数组设置为内部存储数组。

添加元素

CopyOnWriteArrayList内部提供了多个添加元素的方法。
在这里插入图片描述

/**
 * 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) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

从jdk21的源码中可以看出,add(E e)方法内部通过synchronized锁机制(jdk1.8的时候还是用ReentrantLock)来确保同一时间只有一个线程可以执行写操作:

  • 在添加元素之前,CopyOnWriteArrayList 会先获取当前的底层数组的快照,这个复制操作是通过 Arrays.copyOf 方法完成,生成一个新的数组,其长度比原数组大 1。遵循“写时复制”(Copy-On-Write)的策略。
  • 在新复制的数组上,将新元素添加到数组的末尾。
  • 将底层数组的引用指向新创建的数组,这样后续的读操作就会看到新添加的元素。

除了add(E e)方法,还提供了在数组特定位置添加元素add(int index, E element)、在数组头部添加元素addFirst(E e)、在数组尾部添加元素addLast(E e) 相关方法。

删除元素

CopyOnWriteArrayList内部同样提供了多个删除元素的方法。

/**
 * 删除指定索引处的元素,并返回被删除的元素。
 * 
 * @param index 要删除的元素的索引
 * @return 被删除的元素
 * @throws IndexOutOfBoundsException 如果索引超出范围(index < 0 || index >= size())
 */
public E remove(int index) {
    // 使用同步锁(lock)保证线程安全,同一时间只允许一个线程执行写操作
    synchronized (lock) { 
        // 获取当前底层数组的快照
        Object[] es = getArray(); 
        int len = es.length; // 当前数组长度
        
        // 获取要删除的元素(通过辅助方法 elementAt),供后续返回
        E oldValue = elementAt(es, index); 
        
        // 计算需要移动的元素数量:删除位置之后的元素个数
        int numMoved = len - index - 1; 
        
        Object[] newElements; // 定义新数组,用于存储删除后的元素
        
        // 如果删除的是最后一个元素,直接复制前 len-1 个元素到新数组
        if (numMoved == 0) { 
            newElements = Arrays.copyOf(es, len - 1); 
        } 
        // 否则,需要分两步复制:
        // 1. 复制删除位置之前的元素
        // 2. 复制删除位置之后的元素
        else { 
            newElements = new Object[len - 1]; // 创建新数组,长度减1
            // 复制删除位置之前的元素(从原数组的 0 到 index-1)
            System.arraycopy(es, 0, newElements, 0, index); 
            // 复制删除位置之后的元素(从原数组的 index+1 开始,复制到新数组的 index 位置)
            System.arraycopy(es, index + 1, newElements, index, numMoved); 
        }
        
        // 将底层数组的引用指向新数组,使后续操作基于新数组
        setArray(newElements); 
        
        // 返回被删除的元素
        return oldValue; 
    }
}

该方法是按索引删除‌元素,删除操作是写操作,同样需要加锁来确保同一时间只有一个线程能修改列表,避免数据竞争:

  • 在加锁后,CopyOnWriteArrayList 会先获取当前底层数组的快照。通用遵循“写时复制”(Copy-On-Write)的策略。
  • 复制操作通过 Arrays.copyOf方法完成,生成一个与原数组内容相同的新数组。
  • 在新数组上执行删除操作。
  • 将底层数组的引用指向新数组,使后续的读操作看到删除后的结果。

除了remove(int index)方法,还提供了删除数组头部元素removeFirst()、删除数组尾部元素removeLast()、按对象删除元素remove(Object o)、按索引范围删除元素removeRange(int fromIndex, int toIndex)等相关方法。

遍历元素

CopyOnWriteArrayList 的遍历操作基于“快照”机制,实现线程安全与弱一致性。遍历时直接访问底层数组的‌当前快照‌,无需加锁。遍历过程中,若其他线程修改列表(如添加/删除元素),迭代器‌不会感知变化‌,仍基于创建时的快照返回数据。遍历元素的方式主要有三种:

(1)通过迭代器遍历

/**
 * Returns an iterator over the elements in this list in proper sequence.
 *
 * <p>The returned iterator provides a snapshot of the state of the list
 * when the iterator was constructed. No synchronization is needed while
 * traversing the iterator. The iterator does <em>NOT</em> support the
 * {@code remove} method.
 *
 * @return an iterator over the elements in this list in proper sequence
 */
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

迭代器创建时锁定当前数组快照,后续遍历不受写操作影响;迭代过程无需加锁且不支持删除元素操作。遍历过程中若列表被修改,迭代器‌不会抛出 ConcurrentModificationException‌。

示例代码:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

// 获取迭代器(基于当前数组快照)
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
     // 输出:A, B, C(顺序固定,不反映后续修改)
    System.out.println(item);
}

(2)通过 forEach 遍历

/**
 * @throws NullPointerException {@inheritDoc}
 */
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    for (Object x : getArray()) {
        @SuppressWarnings("unchecked") E e = (E) x;
        action.accept(e);
    }
}

示例代码:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

list.forEach(System.out::println);
list.forEach(item -> System.out.println(item));

(3)通过 get(int index) 遍历

/**
 * {@inheritDoc}
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    return elementAt(getArray(), index);
}

示例代码:

CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");

for(String item : list){
    System.out.println(item);
}

for (int i = 0; i < list.size(); i++) {
	// 通过索引直接访问数组
    System.out.println(list.get(i)); 
}

与ArrayList 性能对比

以下是一个多线程并发读写测试代码,用于对比 CopyOnWriteArrayList 和 ArrayList 在并发环境下的性能和安全性差异。代码通过模拟多个线程同时读写列表,观察两者的表现。

public class CopyOnWriteArrayListTest {
    private static final int THREAD_COUNT = 20; // 线程数
    private static final int OPERATION_COUNT = 10000; // 每个线程的操作数
    private static final Random RANDOM = new Random();

    // 测试 CopyOnWriteArrayList
    private static class CopyOnWriteTest implements Runnable {
        private CopyOnWriteArrayList<Integer> list;
        private AtomicInteger successCount;

        public CopyOnWriteTest(CopyOnWriteArrayList<Integer> list, AtomicInteger successCount) {
            this.list = list;
            this.successCount = successCount;
        }

        @Override
        public void run() {
            for (int i = 0; i < OPERATION_COUNT; i++) {
                try {
                    if (RANDOM.nextBoolean()) {
                        list.add(RANDOM.nextInt(1000)); // 随机添加
                    }
                    successCount.incrementAndGet(); // 写成功计数
                } catch (Exception e) {
                    // 忽略失败(理论上 CopyOnWriteArrayList 不会失败)
                    System.out.println("CopyOnWriteArrayList写失败:" + e.getMessage());
                }
            }
        }

        public int getSuccessCount() {
            return successCount.get();
        }
    }

    // 测试普通 ArrayList(非线程安全)
    private static class ArrayListTest implements Runnable {
        private ArrayList<Integer> list;
        private AtomicInteger successCount;

        public ArrayListTest(ArrayList<Integer> list, AtomicInteger successCount) {
            this.list = list;
            this.successCount = successCount;
        }

        @Override
        public void run() {
            for (int i = 0; i < OPERATION_COUNT; i++) {
                try {
                    if (RANDOM.nextBoolean()) {
                        list.add(RANDOM.nextInt(1000));
                    }
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    // 捕获并发修改异常(如 ConcurrentModificationException)
                    System.out.println("ArrayList写失败:" + e.getMessage());
                }
            }
        }

        public int getSuccessCount() {
            return successCount.get();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 测试 CopyOnWriteArrayList写
        CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
        testWrite(list, "CopyOnWriteArrayList");
        testRead(list, "CopyOnWriteArrayList");

        // 测试 ArrayList写
        ArrayList<Integer> list2 = new ArrayList<>();
        testWrite(list2, "ArrayList");
        testRead(list2, "ArrayList");
    }

    private static void testRead(List<Integer> list, String listType) throws InterruptedException {
        AtomicInteger successCount = new AtomicInteger(0);
        Thread[] threadArray1 = new Thread[THREAD_COUNT];
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            threadArray1[i] = new Thread(() -> {
                if (!list.isEmpty()) {
                    for (int j = 0; j < OPERATION_COUNT; j++) {
                        list.get(RANDOM.nextInt(list.size()));
                        successCount.incrementAndGet();
                    }
                }
            });
            threadArray1[i].start();
        }

        for (Thread t : threadArray1) {
            t.join();
        }
        long endTime = System.currentTimeMillis();
        System.out.println(listType + "测试完成,读成功操作数: " + successCount.get() + ",耗时:" + (endTime - startTime));
    }

    private static void testWrite(List<Integer> list, String listType) throws InterruptedException {
        AtomicInteger successCount = new AtomicInteger(0);
        Thread[] threadArray1 = new Thread[THREAD_COUNT];
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < THREAD_COUNT; i++) {
            if ("CopyOnWriteArrayList".equals(listType)) {
                CopyOnWriteTest copyOnWriteTask = new CopyOnWriteTest((CopyOnWriteArrayList<Integer>) list, successCount);
                threadArray1[i] = new Thread(copyOnWriteTask);
                threadArray1[i].start();
            }
            if ("ArrayList".equals(listType)) {
                ArrayListTest arrayListTask = new ArrayListTest((ArrayList<Integer>) list, successCount);
                threadArray1[i] = new Thread(arrayListTask);
                threadArray1[i].start();
            }
        }

        for (Thread t : threadArray1) {
            t.join();
        }
        long endTime = System.currentTimeMillis();
        System.out.println(listType + "测试完成,写成功操作数: " + successCount.get() + ",耗时:" + (endTime - startTime));
    }

}

运行结果:

CopyOnWriteArrayList测试完成,写成功操作数: 200000,耗时:3288
CopyOnWriteArrayList测试完成,读成功操作数: 200000,耗时:19
ArrayList写失败:Index 302 out of bounds for length 244
ArrayList测试完成,写成功操作数: 199999,耗时:28
ArrayList测试完成,读成功操作数: 200000,耗时:18

从运行结果可以看出,CopyOnWriteArrayList在读写性能上并不具备优势,而是在线程安全方面保证了数据的准确性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值