并发容器学习—CopyOnWriteArrayList与CopyOnWriteArraySet

本文主要介绍了CopyOnWriteArrayList并发容器,包括其底层数据结构为数组,增删改通过复制数组实现,读写分离,写加锁、读无锁,适合读多写少场景。还提到CopyOnWriteArraySet底层由CopyOnWriteArrayList实现,特点类似。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、CopyOnWriteArrayList并发容器

1.CopyOnWriteArrayList的底层数据结构

    CopyOnWriteArrayList的底层实现是数组,CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其增删改操作都是通过对底层数组的重新复制来实现的。这种容器内存开销很大,但是在读操作频繁(查询),写操作(增删改)极少的场合却极用,并且每一次写操作都是在一块新的空间上进行的,不存在并发问题。

 

2.CopyOnWriteArrayList的继承关系

    CopyOnWriteArrayList继承关系如下图所示,List接口和Collection接口在学习ArrayList时已经分析过,不在多说。

24b0b16c98766bcd6a95e9f15fbf952d950.jpg

3.重要属性和构造方法

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    //可重入锁
    final transient ReentrantLock lock = new ReentrantLock();

    //底层实现,用于存放数据的数组
    private transient volatile Object[] array;

    //获取数组
    final Object[] getArray() {
        return array;
    }

    //修改数组
    final void setArray(Object[] a) {
        array = a;
    }

    //默认构造器,初始化一个长度为0 的数组
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    //将集合c中所有元素转化成数组的形式,存储与当前List中
    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);
    }

    //将数组toCopyIn拷贝以一份到array中
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
}

4.add与set过程

    CopyOnWriteArrayList在新增元素时,会先进行加锁操作,以保证写操作的并发安全

//将新增数据加到数组末尾
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();    //解锁
    }
}

//新增元素到指定位置
public void add(int index, E element) {
    final ReentrantLock lock = this.lock;    //获取重入锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (index > len || index < 0)    //判断索引是否合法
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        //判断插入的位置是否是数组末尾
        if (numMoved == 0)    
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            newElements = new Object[len + 1];
            //将原数组中0到index的元素复制到新数组的0到index位置
            System.arraycopy(elements, 0, newElements, 0, index);
            //将原数组的index之后的数据复制到新数组的index+1位置之后
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;    //新增element
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

//修改index索引上的数据
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();
    }
}

5.get的过程

    CopyOnWriteArrayList的读取过程并没加锁,这是因为CopyOnWriteArrayList的读过程是天然线程安全的,所有的写操作都是在一块新的内存空间上,而读操作则是在原有的空间上进行的,不会出现并发问题,读写天然分离。

public E get(int index) {
    return get(getArray(), index);
}

private E get(Object[] a, int index) {
    return (E) a[index];
}

6.remove的过程

    删除的过程其实以新增修改一样,都是新建数组实现的。

//根据索引删除数据
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();
    }
}

//将o从数组中删除
public boolean remove(Object o) {
    Object[] snapshot = getArray();
    int index = indexOf(o, snapshot, 0, snapshot.length);    //获取o的索引位置
    
    //判断index是否合法
    return (index < 0) ? false : remove(o, snapshot, index);
}

//将snapshot中index位置的数据o删除
private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();    //获取当前数组
        int len = current.length;
        
        //判断数组是否发生改变,即是否有其他线程将数组修改了
        if (snapshot != current) findIndex: {
            //比较修改后的数组长度与index索引的大小,取小的值
            int prefix = Math.min(index, len);

            //遍历current数组查找第一个与快照数组元素不同的索引
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }

            //o元素索引大于等于current数组的长度
            //说明current数组中不存在o元素
            if (index >= len)
                return false;

            //o元素的索引仍在current数组中
            //且要删除元素的索引的没有发生改变
            if (current[index] == o)
                break findIndex;

            //o元素索引发生改变,重新获取o元素的索引
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        Object[] newElements = new Object[len - 1];
        System.arraycopy(current, 0, newElements, 0, index);
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

7.总结

    由上面的分析可得出CopyOnWriteArrayList具有如下特点:

    1.CopyOnWriteArrayList是线程安全的容器,读写分离,写数据时通过ReentrantLock锁定一个线程拷贝数组元素进行增删改操作;读数据时则不需要同步控制。

    2.CopyOnWriteArrayList中是允许null数据和重复数据的

    3.CopyOnWriteArrayList的并发安全性是通过创建新的数组和重入锁实现的,会耗费大量内存空间,只适合读多谢写少,数据量不大的环境。

 

二、CopyOnWriteArraySet并发容器

1.CopyOnWriteArraySet

    由于CopyOnWriteArraySet的底层是通过CopyOnWriteArrayList来实现的,其特点与CopyOnWriteArrayList,因此不再做过多的分析。

 

 

转载于:https://my.oschina.net/bzhangpoorman/blog/3044787

<think>我们正在讨论非并发容器并发容器的转换方法及其原理。首先,我们需要明确非并发容器(如ArrayList、HashMap等)和并发容器(如ConcurrentHashMap、CopyOnWriteArrayList等)的设计差异。 非并发容器并发环境下需要外部同步(例如使用synchronized或ReentrantLock)来保证线程安全。而并发容器内部已经实现了线程安全的机制,可以在多线程环境下直接使用。 转换方法: 1. **非并发容器并发容器**:通过包装器或直接使用并发容器实现。 - 对于List:可以使用`Collections.synchronizedList`方法将非并发List(如ArrayList)转换为同步List,但这并不是真正的并发容器(它使用同步块)。或者直接使用`CopyOnWriteArrayList`。 - 对于Map:可以使用`Collections.synchronizedMap`将HashMap转换为同步Map,或者直接使用`ConcurrentHashMap`。 2. **并发容器转非并发容器**:通常不推荐,因为并发容器内部机制复杂,直接转换为非并发容器可能失去线程安全特性。但可以通过复制数据的方式,例如将ConcurrentHashMap的所有元素复制到一个新的HashMap中(注意:在复制过程中需要保证没有并发修改,或者使用快照方式)。 原理分析: - **非并发容器转换为同步容器(使用Collections.synchronizedXXX)**: 原理:这些包装器在容器的方法上加上synchronized同步块,使用一个全局锁(mutex)来保证线程安全。例如: ```java // Collections.synchronizedList 源码片段 public E get(int index) { synchronized (mutex) {return list.get(index);} } ``` 缺点:全局锁导致并发性能较低。 - **使用并发容器**: - `CopyOnWriteArrayList`:写操作时复制整个底层数组,读操作无锁。适合读多写少的场景。 - `ConcurrentHashMap`:在JDK1.7中采用分段锁(Segment),在JDK1.8中采用CAS+synchronized(锁单个桶)。 下面我们详细讨论转换方法和原理。 ### 一、非并发容器转换为线程安全容器 #### 1. 使用`Collections.synchronizedXXX`方法 Java提供了一组静态方法,可以将非并发容器包装成同步容器。这些方法包括: - `synchronizedList(List<T> list)` - `synchronizedMap(Map<K,V> m)` - `synchronizedSet(Set<T> s)` **原理**: - 在返回的包装器对象中,每个方法都使用一个互斥锁(mutex)进行同步。默认情况下,互斥锁是包装器对象本身(this)。 - 例如,`synchronizedList`的实现: ```java static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> { final List<E> list; SynchronizedList(List<E> list) { super(list); this.list = list; } public E get(int index) { synchronized (mutex) { return list.get(index); } } // 其他方法类似,都加上了synchronized块 } ``` **缺点**: - 性能瓶颈:所有操作都需要获取同一把锁,高并发下竞争激烈。 - 迭代器需要外部同步:在迭代时,必须手动对容器加锁,否则可能抛出`ConcurrentModificationException`。 #### 2. 使用并发容器替代 直接使用Java并发包(`java.util.concurrent`)中的容器类: - `List` → `CopyOnWriteArrayList` - `Map` → `ConcurrentHashMap` - `Set` → `CopyOnWriteArraySet` 或 `ConcurrentHashMap.newKeySet()` - `Queue` → `ConcurrentLinkedQueue`, `ArrayBlockingQueue`等 **原理**: - **`CopyOnWriteArrayList`**:写操作(add、set、remove等)通过复制整个数组来实现,写操作在一个锁的保护下进行(保证只有一个写线程),读操作无锁。适用于读多写少的场景[^4]。 ```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(); } } ``` - **`ConcurrentHashMap`(JDK1.8)**:采用数组+链表/红黑树结构,使用CAS和synchronized(锁住单个桶)来实现线程安全。put操作流程如下[^3]: 1. 计算key的hash值。 2. 如果表未初始化,则初始化。 3. 如果定位到的桶为空,则通过CAS尝试插入。 4. 如果桶不为空(有节点),则使用synchronized锁住该桶的第一个节点,然后进行链表或红黑树的插入。 5. 如果链表长度超过8,则转换为红黑树。 6. 如果总元素数量超过阈值,则扩容。 ### 二、并发容器转换为非并发容器 通常没有直接的方法,因为并发容器内部结构复杂,且设计目标就是支持并发。但可以通过以下方式: 1. **复制数据**:在并发容器上获取一个快照,然后复制到一个非并发容器中。例如: ```java Map<K, V> map = new HashMap<>(); concurrentHashMap.forEach((k, v) -> map.put(k, v)); ``` 注意:在复制过程中,如果有并发修改,可能会丢失更新。因此,最好在无并发修改时进行(如暂停写操作)。 2. **使用序列化/反序列化**:将并发容器序列化为字节流,再反序列化为非并发容器(同样要注意数据一致性)。 ### 三、设计思想对比 | 容器类型 | 线程安全实现方式 | 适用场景 | |------------------|------------------------------|------------------------------| | 同步包装容器 | 方法级synchronized(全局锁) | 低并发,简单场景 | | `CopyOnWriteArrayList` | 写时复制(复制整个数组) | 读多写少,读操作要求快速响应 | | `ConcurrentHashMap` | CAS + 桶锁(synchronized) | 高并发读写 | > **转换建议**: > - 需要线程安全且性能要求高 → 直接使用并发容器(如`ConcurrentHashMap`) > - 兼容旧代码且并发压力小 → 使用`Collections.synchronizedXXX` > - 不需要线程安全 → 直接使用非并发容器(但需确保单线程环境) ### 总结 非并发容器并发容器的转换本质上是线程安全机制的转换。转换方法包括: 1. **升级为线程安全**: - 通过包装器(`Collections.synchronizedXXX`)添加全局锁。 - 替换为并发容器(如`ConcurrentHashMap`、`CopyOnWriteArrayList`)。 2. **降级为非线程安全**:通过复制数据创建非并发容器副本。 选择哪种方式取决于具体场景: - 并发性能要求高 → 并发容器。 - 需要简单同步 → 包装器。 - 单线程使用 → 非并发容器。 --- §§相关问题§§ 1. `Collections.synchronizedList` 和 `CopyOnWriteArrayList` 在性能上有何差异? 2. `ConcurrentHashMap` 在 JDK1.7 和 JDK1.8 中的实现原理有何不同? 3. 如何选择 `ConcurrentHashMap` 和 `Hashtable`? 4. `CopyOnWriteArrayList` 的写时复制机制在哪些场景下不适用?</think>### 非并发容器并发容器的转换方法及原理 Java 容器分为非并发容器(`java.util`)和并发容器(`java.util.concurrent`)。二者转换的核心是**通过同步机制实现线程安全**或**利用并发容器替代非并发容器**。以下是转换方法原理分析: --- #### 一、非并发容器并发安全的容器 ##### 1. **外部同步包装(显式加锁)** **方法**:使用 `synchronized` 或 `ReentrantLock` 包装非并发容器的操作。 **原理**: - 通过互斥锁保证同一时刻只有一个线程操作容器。 - 示例: ```java List<String> syncList = Collections.synchronizedList(new ArrayList<>()); // 包装为同步List[^1] Map<K,V> syncMap = Collections.synchronizedMap(new HashMap<>()); ``` **源码分析**(以 `Collections.synchronizedList` 为例): ```java public boolean add(E e) { synchronized (mutex) { // 全局锁(mutex = this) return list.add(e); // 委托给原始ArrayList } } ``` **缺点**: - 全局锁导致并发性能低(所有操作串行化)[^1]。 - 迭代器需手动同步,否则可能抛出 `ConcurrentModificationException`。 ##### 2. **写时复制(Copy-On-Write)** **方法**:将非并发容器替换为 `CopyOnWriteArrayList` 或 `CopyOnWriteArraySet`。 **原理**: - **读操作**:无锁访问原数组(快照)。 - **写操作**:复制新数组修改,替换旧引用(加锁保证原子性)[^4]。 **数据结构**: ```java // CopyOnWriteArrayList 核心字段 final transient ReentrantLock lock = new ReentrantLock(); private transient volatile Object[] array; // volatile保证可见性 ``` **操作流程**: ```java public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); // 加锁 try { Object[] elements = getArray(); Object[] newElements = Arrays.copyOf(elements, len + 1); // 复制新数组 newElements[len] = e; setArray(newElements); // volatile写替换引用 return true; } finally { lock.unlock(); } } ``` **适用场景**:读多写少(如监听器列表)。 --- #### 二、并发容器 → 非并发容器 ##### 1. **数据迁移(快照复制)** **方法**:将并发容器的数据复制到非并发容器。 **原理**: - 利用并发容器的原子快照(如 `ConcurrentHashMap.keySet()` 或 `CopyOnWriteArrayList` 的数组)。 - 示例: ```java ConcurrentHashMap<String, Integer> concurrentMap = ...; HashMap<String, Integer> hashMap = new HashMap<>(concurrentMap); // 复制构造 ``` **注意**: - 复制过程非原子,可能丢失实时更新。 - 适用于只读场景或低一致性要求。 ##### 2. **单线程环境降级** **方法**:在确认单线程访问时直接使用并发容器。 **原理**: - 并发容器(如 `ConcurrentHashMap`)在单线程下无锁竞争开销。 - 性能并发容器接近(JDK8 优化了 `ConcurrentHashMap` 的内存布局)。 --- #### 三、并发容器替代方案(推荐) 直接使用并发容器替代非并发容器,无需转换: | 非并发容器 | 并发容器替代品 | 实现原理 | |----------------|---------------------------|-----------------------------| | `ArrayList` | `CopyOnWriteArrayList` | 写时复制 + ReentrantLock | | `HashMap` | `ConcurrentHashMap` | CAS + synchronized 桶锁[^3] | | `HashSet` | `ConcurrentHashMap.KeySet` | 委托给ConcurrentHashMap | | `LinkedList` | `ConcurrentLinkedQueue` | CAS 无锁队列 | ##### `ConcurrentHashMap` 原理(JDK8)[^3] **数据结构**: - 数组 + 链表/红黑树(同 `HashMap`)。 **线程安全机制**: - **CAS**:初始化数组、插入空桶等。 - **synchronized**:锁定哈希冲突的桶(链表头节点)。 **put 流程**: 1. 计算哈希,定位桶位置。 2. 桶为空 → CAS 插入。 3. 桶非空 → `synchronized` 锁住头节点: - 链表 → 遍历插入/更新 - 红黑树 → 树节点插入 4. 链表长度 ≥ 8 → 树化。 --- ### 设计思想对比 | **方案** | **原理** | **性能** | **适用场景** | |--------------------|------------------------------|---------------|---------------------| | 外部同步包装 | 全局锁(synchronized) | 低并发 | 简单同步需求 | | 写时复制 | 读无锁 + 写复制数组 | 读极高,写极低 | 读多写少(如配置) | | `ConcurrentHashMap`| CAS + 桶级锁 | 高并发 | 高频读写键值对 | | 数据迁移 | 原子快照复制 | 一次性开销 | 只读数据导出 | --- ### 关键问题解答 #### 1. 为什么推荐直接使用并发容器? - **安全**:内置线程安全机制,避免手动同步错误。 - **性能**:并发容器优化了锁粒度(如 `ConcurrentHashMap` 的桶锁)。 - **功能**:提供原子方法(如 `ConcurrentHashMap.putIfAbsent()`)。 #### 2. 如何选择转换方案? - 需要兼容旧代码 → 外部同步包装。 - 高频读、低频写 → `CopyOnWriteArrayList`。 - 高频读写 → `ConcurrentHashMap`。 - 数据归档 → 快照复制到非并发容器。 > **最佳实践**: > - 新项目优先使用 `java.util.concurrent` 容器。 > - 转换时测试并发场景下的性能和数据一致性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值