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

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

目录

一、介绍

二、特性

1. 线程安全

2. 高效读取

3. 低效写入

4. 适合读多写少的场景

三、实现原理

四、注意事项

1. 内存占用

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

3. 不适用于实时数据

4. 并发性能问题

5. 迭代器弱一致性问题

五、应用场景

六、CopyOnWriteArrayList和ArrayList区别

1. 线程安全性

2. 内存占用

3. 写入操作的开销

七、实际应用


一、介绍

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

二、特性

1. 线程安全

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

2. 高效读取

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

3. 低效写入

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

4. 适合读多写少的场景

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

三、实现原理

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

四、注意事项

1. 内存占用

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

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

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

3. 不适用于实时数据

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

4. 并发性能问题

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

5. 迭代器弱一致性问题

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

五、应用场景

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

六、CopyOnWriteArrayList和ArrayList区别

1. 线程安全性

ArrayList是非线程安全的容器,如果多个线程同时对其进行读写操作,可能会导致数据不一致的问题。

CopyOnWriteArrayList是线程安全的容器,可以保证在并发环境下数据的一致性。

2. 内存占用

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

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

3. 写入操作的开销

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

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

因此,如果需要在高并发环境下使用集合,可以考虑使用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,就不会出现这个问题!

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

懒阳快跑

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

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

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

打赏作者

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

抵扣说明:

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

余额充值