导读:本期聚焦于小伙伴创作的《Java CopyOnWriteArraySet如何保证线程安全?深入解析写时复制与锁机制》,敬请观看详情,探索知识的价值。以下视频、文章将为您系统阐述其核心内容与价值。如果您觉得《Java CopyOnWriteArraySet如何保证线程安全?深入解析写时复制与锁机制》有用,将其分享出去将是对创作者最好的鼓励。

Java CopyOnWriteArraySet如何保证线程安全

在多线程并发场景下,集合类的线程安全问题一直是开发者需要重点关注的内容。Java并发包java.util.concurrent中提供了很多线程安全的集合实现,CopyOnWriteArraySet就是其中专门针对Set场景设计的线程安全集合,它通过对写操作加锁和写时复制的机制,在保证线程安全的同时,还能为读操作提供极高的性能。

CopyOnWriteArraySet的核心实现原理

CopyOnWriteArraySet的底层实现完全依赖于CopyOnWriteArrayList,它没有自己的独立数据结构,所有操作都会委托给内部持有的CopyOnWriteArrayList实例来完成。要理解它的线程安全机制,首先要了解CopyOnWriteArrayList的核心设计思想:写时复制(Copy-On-Write)

简单来说,写时复制指的是:当需要对集合进行修改操作(新增、删除、修改等)时,不会直接修改原有的底层数组,而是先将原有数组复制一份新的数组,在新的数组上完成修改操作,修改完成后再将内部引用指向新的数组。而读操作则直接访问当前的底层数组,不需要任何加锁操作。

写操作的线程安全保障

CopyOnWriteArraySet的所有写操作(比如add、remove等)最终都会调用CopyOnWriteArrayList对应的写方法,这些写方法都使用了ReentrantLock可重入锁来保证同一时间只有一个线程能执行写操作,避免多个线程同时复制数组导致的数组覆盖问题。

我们以add方法为例,看具体的实现逻辑:

public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    // 内部持有的CopyOnWriteArrayList实例,所有操作委托给它
    private final CopyOnWriteArrayList<E> al;

    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

    /**
     * 添加元素,保证Set不重复的特性
     */
    public boolean add(E e) {
        // 直接调用CopyOnWriteArrayList的addIfAbsent方法
        // 该方法会先判断元素是否存在,不存在才添加,保证Set的唯一性
        return al.addIfAbsent(e);
    }
}

接下来看CopyOnWriteArrayList中addIfAbsent方法的实现,这就是写操作线程安全的核心:

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 可重入锁,保证写操作的互斥性
    final transient ReentrantLock lock = new ReentrantLock();
    // 底层存储数组,用volatile修饰,保证多线程之间的可见性
    private transient volatile Object[] array;

    /**
     * 添加元素,如果元素不存在则添加,返回是否添加成功
     */
    public boolean addIfAbsent(E e) {
        Object[] snapshot = array;
        // 先判断元素是否已经存在,不存在才执行添加逻辑
        if (indexOf(e, snapshot, 0, snapshot.length) >= 0) {
            return false;
        }
        // 获取锁,保证同一时间只有一个线程执行写操作
        lock.lock();
        try {
            // 再次获取当前数组,因为可能在获取锁之前有其他线程修改了数组
            Object[] current = array;
            int len = current.length;
            // 如果数组没被修改,直接使用当前数组复制
            if (snapshot == current) {
                Object[] newElements = new Object[len + 1];
                // 复制原有数组内容到新数组
                System.arraycopy(current, 0, newElements, 0, len);
                // 添加新元素到新数组末尾
                newElements[len] = e;
                // 将底层数组引用指向新的数组
                array = newElements;
                return true;
            } else {
                // 如果数组已经被修改,重新判断元素是否存在
                int common = Math.min(len, snapshot.length);
                for (int i = 0; i < common; i++) {
                    if (current[i] != snapshot[i] && eq(e, current[i])) {
                        return false;
                    }
                }
                for (int i = common; i < len; i++) {
                    if (eq(e, current[i])) {
                        return false;
                    }
                }
                // 元素不存在,复制数组添加新元素
                Object[] newElements = new Object[len + 1];
                System.arraycopy(current, 0, newElements, 0, len);
                newElements[len] = e;
                array = newElements;
                return true;
            }
        } finally {
            // 释放锁,避免死锁
            lock.unlock();
        }
    }

    /**
     * 判断两个元素是否相等,处理null的情况
     */
    private static boolean eq(Object o1, Object o2) {
        return (o1 == null) ? o2 == null : o1.equals(o2);
    }
}

从上面的代码可以看出,写操作的线程安全保障来自两点:

  • 使用ReentrantLock锁,保证同一时间只有一个线程能执行复制数组、修改引用的写操作,避免并发写导致的数组覆盖问题。
  • 修改完成后将新的数组赋值给用volatile修饰的array变量,保证新数组对其他线程的可见性,其他线程后续读操作能立刻看到新的数组内容。

读操作的线程安全保障

CopyOnWriteArraySet的读操作(比如contains、size、iterator等)同样委托给CopyOnWriteArrayList,而CopyOnWriteArrayList的读操作不需要任何加锁,直接访问当前的底层数组即可。

因为写操作是通过复制新数组、替换引用的方式完成的,所以读操作访问的要么是旧的数组,要么是新的数组,都不会出现读到修改一半的数据的情况,天然保证了读操作的线程安全。同时,由于读操作不需要加锁,在高并发读的场景下,性能会比加锁的同步集合(比如Collections.synchronizedSet)好很多。

我们来看CopyOnWriteArrayList中读操作的示例:

public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    // 底层存储数组,用volatile修饰,保证多线程之间的可见性
    private transient volatile Object[] array;

    /**
     * 获取指定位置的元素,读操作无锁
     */
    public E get(int index) {
        return get(array, index);
    }

    /**
     * 私有方法,从指定数组中获取指定位置的元素
     */
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * 判断集合是否包含指定元素,读操作无锁
     */
    public boolean contains(Object o) {
        Object[] elements = array;
        return indexOf(o, elements, 0, elements.length) >= 0;
    }
}

这里需要注意的是,CopyOnWriteArraySet返回的迭代器也是基于创建迭代器时的底层数组快照实现的,迭代过程中不会抛出ConcurrentModificationException异常,因为迭代器遍历的是旧数组,后续即使有写操作修改了集合,也不会影响正在遍历的迭代器。

适用场景与注意事项

CopyOnWriteArraySet的线程安全机制决定了它的适用场景:

  • 适合读多写少的并发场景,比如缓存、配置信息存储等,读操作不需要加锁,性能很高。
  • 不适合写操作非常频繁的场景,因为每次写操作都需要复制整个数组,如果数组很大或者写操作很多,会产生大量的内存拷贝开销,甚至频繁触发GC。

另外需要注意,CopyOnWriteArraySet只能保证单个操作的线程安全,如果需要执行多个操作的组合(比如先判断元素是否存在,再添加元素),仍然需要额外的同步手段,因为它提供的单个操作是原子的,但组合操作不是原子的。

总结

CopyOnWriteArraySet通过底层依赖CopyOnWriteArrayList,采用写时复制+可重入锁的机制保证线程安全:写操作通过加锁保证同一时间只有一个线程修改,修改时复制新数组再替换引用,避免影响读操作;读操作直接访问底层数组,不需要加锁,性能优异。开发者在使用时需要根据实际的读写比例选择合适的场景,才能发挥出它的最大优势。

CopyOnWriteArraySet线程安全写时复制ReentrantLock并发集合修改时间:2026-05-24 13:05:54

免责声明:已尽一切努力确保本网站所含信息的准确性。网站部分内容来源于网络或由用户自行发表,内容观点不代表本站立场。本站是个人网站免费分享,内容仅供个人学习、研究或参考使用,如内容中引用了第三方作品,其版权归原作者所有。若内容触犯了您的权益,请联系我们进行处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。前端、网络、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握网站开发与运维所需的核心技术栈。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端逻辑,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。