0
点赞
收藏
分享

微信扫一扫

线程安全的集合CopyOnWriteArrayList【Java多线程必备】

 点击  Mr.绵羊的知识星球 解锁更多优质文章。

一、介绍

CopyOnWriteArrayList是Java集合框架中的一种并发容器。它是ArrayList的线程安全版本,采用了一种特殊的“写时复制”策略,即每次修改操作都会先复制一份当前集合,然后再对新的副本进行修改,最后再用新的副本替换旧的副本。这种策略能够保证并发操作时不会影响到其他线程的读操作,从而保证线程安全性。

二、特性

1. 线程安全

CopyOnWriteArrayList是线程安全的,并发访问时不需要使用额外的同步机制,因为每个线程访问的都是不同的副本。

2. 高效读取

由于读取操作不需要加锁,所以CopyOnWriteArrayList在读取操作上具有很高的性能。

3. 低效写入

每次修改都需要复制一份新的副本,因此写入操作相对较慢,特别是在集合较大的情况下。

4. 适合读多写少的场景

CopyOnWriteArrayList适合于读多写少的场景,例如日志、观察者模式等。因为写入操作会产生较大的开销,所以在高并发写入的场景下不适合使用。

三、实现原理

CopyOnWriteArrayList采用了“写时复制”的策略,每次修改都会先复制一份当前集合,然后再对新的副本进行修改。这个过程中,其他线程的读操作仍然是访问旧的副本,直到修改完成后再使用新的副本替换旧的副本。

四、注意事项

1. 内存占用

由于每次修改都需要复制一份新的副本,因此CopyOnWriteArrayList在内存占用上相对较高,尤其是在集合较大的情况下。

2. 迭代器不支持修改操作

CopyOnWriteArrayList的迭代器不支持修改操作,如果需要修改集合中的元素,需要使用集合本身提供的修改方法。

3. 不适用于实时数据

由于CopyOnWriteArrayList的写入操作有一定的延迟,因此不适用于实时数据的场景,例如股票、期货等实时数据的更新。

4. 并发性能问题

虽然CopyOnWriteArrayList在读操作上具有良好的并发性能,但是在写操作上由于每次修改都需要复制一份新的数组,因此性能会受到影响。因此在高并发场景下,建议使用其他的并发容器,例如ConcurrentHashMap等。

5. 迭代器弱一致性问题

由于CopyOnWriteArrayList的迭代器是在快照数组上进行遍历,因此可能存在迭代器遍历到的数据已经被其他线程修改的情况。因此需要注意CopyOnWriteArrayList的迭代器只提供弱一致性的保证,不支持并发修改操作。

五、应用场景

适合于读多写少的场景,例如日志、观察者模式等。同时,由于它的写入操作相对较慢,在高并发写入的场景下不适合使用。

六、CopyOnWriteArrayList和ArrayList区别

1. 线程安全性

可能会导致数据不一致的问题。

保证在并发环境下数据的一致性。

2. 内存占用

在数组中间插入或删除元素,则需要对数组进行扩容或移动,会造成一定的开销。

每次写入时都会复制一份新的副本,因此会占用较多的内存空间。

3. 写入操作的开销

插入、删除操作时,需要对数组进行移动或扩容,操作的开销较大。

写入操作时,需要先进行一次数组复制,然后再对新数组进行修改,开销也比较大。

高并发环境下使用集合,可以考虑使用CopyOnWriteArrayList;如果不需要线程安全性,并且写操作比较频繁,可以选择使用ArrayList。

具体实现可以看下面的CopyOnWriteArrayList伪代码:

class CopyOnWriteArrayList<T> {
    // 一个可重入锁,用于实现线程安全
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    // 只能通过 getArray/setArray 方法访问的基础数组
    private transient volatile Object[] array;

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

在add方法中,每次修改都会复制一份新的副本,并对新的副本进行修改,最后再用新的副本替换旧的副本。这个过程中使用了ReentrantLock关键字来保证线程安全。

七、实际应用

1. 案例一

(1) 场景

验证CopyOnWriteArrayList和ArrayList在多线程情况下的区别。

(2) 代码

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * CopyOnWriteArrayListCase1: CopyOnWriteArrayList相对于ArrayList有什么根本区别呢?
 * CopyOnWriteArrayList: 线程安全。
 * ArrayList: 线程不安全。
 * 下面这个案例就是验证CopyOnWriteArrayList和ArrayList在多线程情况下修改的区别。
 *
 * @author wxy
 * @since 2023-04-26
 */
public class CopyOnWriteArrayListCase1 {
    public static void main(String[] args) {
        // 创建一个CopyOnWriteArrayList
        // List<Integer> numbers = new CopyOnWriteArrayList<>();
        // 创建一个ArrayList
        List<Integer> numbers = new ArrayList<>();

        // 添加元素到该ArrayList中
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        // 启动第一个线程,修改List元素,每2秒执行一次。
        new Thread(() -> {
            try {
                while (true) {
                    //执行修改操作,增加一个元素。
                    numbers.add(6);
                    //暂停2秒
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        //启动第二个线程,遍历List,每1秒执行一次。
        new Thread(() -> {
            try {
                while (true) {
                    for (Integer number : numbers) {
                        // System.out.println("used CopyOnWriteArrayList: num: " + number);
                        System.out.println("used ArrayList: num: " + number);
                    }
                    //暂停1秒
                    TimeUnit.SECONDS.sleep(1);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

在以上程序中,我们首先创建了一个名为numbers的ArrayList,并在其中添加了一些元素。接着,我们启动了两个线程,一个线程每1秒向ArrayList中添加元素,另一个线程每1秒遍历ArrayList中的元素并打印它们。由于这两个线程都同时访问同一个ArrayList,因此会发生并发修改异常,导致程序抛出ConcurrentModificationException异常,从而使程序崩溃。

线程安全的集合CopyOnWriteArrayList【Java多线程必备】_CopyOnWriteArrayList

如果我们使用CopyOnWriteArrayList,就不会出现这个问题!

线程安全的集合CopyOnWriteArrayList【Java多线程必备】_多线程必备_02


举报

相关推荐

0 条评论