J.U.C Review - CopyOnWrite容器

文章目录

什么是CopyOnWrite容器

CopyOnWrite容器是一种实现了写时复制(Copy-On-Write,COW)机制的并发容器。在并发场景中,多个线程可能同时访问同一资源,当某个线程需要修改数据时,系统会创建该数据的副本供其修改,而其他线程仍然可以访问原始数据。这种机制的主要优点是可以在读操作频繁的情况下,避免加锁,从而提高读取性能。

在Java中,从JDK 1.5开始,提供了两个主要的CopyOnWrite容器:CopyOnWriteArrayList和CopyOnWriteArraySet。这两个容器的设计使得在"读多写少"的场景下,能够有效地提高并发性能。


CopyOnWriteArrayList

优点

  1. 无需同步 :CopyOnWriteArrayList在进行读取操作时不需要任何同步措施,极大地提高了读取性能。

  2. 避免异常 :在遍历时,即使有其他线程对列表进行修改,也不会抛出ConcurrentModificationException异常,因为读取和写入操作分别作用于不同的容器。

缺点

  1. 内存开销:每次写操作都会复制整个容器,导致内存使用增加,可能引发频繁的垃圾回收(GC)问题。

  2. 数据一致性问题:由于写操作和读操作作用于不同的容器,读操作可能读取到旧数据,因此不能保证实时一致性。

源码示例

CopyOnWriteArrayList的add和remove方法的实现逻辑如下:

java 复制代码
public boolean add(E e) {
   
    // ReentrantLock加锁,保证线程安全
    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 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)
                // 如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
                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();
        }
    }

再来看看CopyOnWriteArrayList效率最高的读操作的源码

java 复制代码
public E get(int index) {
    return get(getArray(), index);
}
java 复制代码
 private E get(Object[] a, int index) {
     return (E) a[index];
 }

由上可见"读操作"是没有加锁,直接读取。


仿写:CopyOnWriteMap的实现

虽然Java并发包中没有提供CopyOnWriteMap,但可以参考CopyOnWriteArrayList实现一个简单的版本。以下是一个基本的实现示例:

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class CopyOnWriteMap<K, V> implements Map<K, V> {
    private volatile Map<K, V> internalMap = new HashMap<>();

    public V put(K key, V value) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<>(internalMap);
            V oldValue = newMap.put(key, value);
            internalMap = newMap;
            return oldValue;
        }
    }

    public V get(Object key) {
        return internalMap.get(key);
    }

    public void putAll(Map<? extends K, ? extends V> m) {
        synchronized (this) {
            Map<K, V> newMap = new HashMap<>(internalMap);
            newMap.putAll(m);
            internalMap = newMap;
        }
    }
}

CopyOnWriteMap适用于"读多写少"的场景,例如一个搜索网站的黑名单管理系统。黑名单在特定时间更新,而在用户搜索时,系统需要快速检查关键字是否在黑名单中。


注意事项

使用CopyOnWrite容器时需要注意:

  1. 内存开销:应根据实际需求初始化容器的大小,以减少扩容带来的开销。

  2. 数据一致性 :CopyOnWrite容器只能保证最终一致性,不能保证实时一致性,因此不适用于需要立即读取写入数据的场景。

相关推荐
学长毕设5 小时前
基于SpringBoot的实验室资产管理系统
java·spring boot·后端
zzu123zsw6 小时前
从臃肿的模板调用到声明式契约:深入 UniHttp 动态代理与多渠道生命周期扩展
java·spring boot·microsoft·unihttp
源码集结号7 小时前
云诊所门诊系统源码,基于SpringBoot+Vue.js架构,支持多租户设计与Docker容器化部署
java·spring boot·源码·医院信息系统·门诊系统·诊所系统
论文复现现场7 小时前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
啃瓜子的松鼠7 小时前
泛微OAe9-后端二开异步调用
java
秋天的怀念_David8 小时前
第二十一夜 · 数字黑洞与西西弗斯的惩罚
java·程序人生·算法
外收内放8 小时前
Python基础语法练习题(73)
java·前端·python
Wang's Blog8 小时前
Java 项目实战: 交友项目 - 环信后台发送消息测试
java·交友
用户3721574261359 小时前
Java 操作 Word 脚注和尾注:添加、读取与删除
java
小嘉丶学长9 小时前
Day14 · 字典与集合
java·开发语言·网络·python·学习·1024程序员节