zoukankan      html  css  js  c++  java
  • Java 集合框架(二):CopyOnWriteArrayList

    上一章节我们说过,Vector 是同步容器,我们编码时的非原子操作仍然不能保证线程安全。这一节我们就介绍一个线程安全的同步容器。

    写入时复制(CopyOnWrite)思想

    写入时复制,CopyOnWrite 简称 COW 思想时计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者同时要求相同的资源(如内存或者是磁盘上的数据),他们会获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本 给该调用者,而其他调用者所见到的最初的资源仍然不变。

    此做法的主要有点是如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。

    概要

    1. CopyOnWriteArrayList 是线程安全容器,底层通过复制数组的方式来实现。
    2. CopyOnWriteArrayList 在遍历的时候不会抛出 ConcrrentModificationException,并且遍历的时候不用加锁。
    3. 元素可以为 null。

    成员变量

    /** 可重入锁对象 */
    final transient ReentrantLock lock = new ReentrantLock();
    
    /** CopyOnWriteArrayList底层由数组实现,volatile修饰 */
    private transient volatile Object[] array;
    
    /**
         * 得到数组
         */
    final Object[] getArray() {
        return array;
    }
    
    /**
         * 设置数组
         */
    final void setArray(Object[] a) {
        array = a;
    }
    
    /**
         * 初始化CopyOnWriteArrayList相当于初始化数组
         */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
    
    

    CopyOnWriteArrayList 的底层实现就是数组加 ReentrantLock。

    add 方法

    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;
    
            // 将volatile Object[] array 的指向替换成新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
    

    添加元素时加上 lock 锁,并复制一个新数组,增加操作在新数组上完成,然后将 array 指向新数组,最后解锁。

    get 方法

    直接读取数组。

    public E get(int index) {
        return get(getArray(), index);
    }
    
    final Object[] getArray() {
        return array;
    }
    

    set 方法与 add 类似,这里就不贴出代码了。

    总结:

    在修改时,复制出一个新的数组,修改的操作在新的数组中完成,最后将 array 指向新的数组。

    写加锁,读不加锁。

    遍历时为什么不用加锁

    我们来看看在容器遍历时对其修改为什么不会抛出异常。

    // 1. 返回的迭代器是COWIterator
    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }
    
    
    // 2. 迭代器的成员属性
    private final Object[] snapshot;
    private int cursor;
    
    // 3. 迭代器的构造方法
    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }
    
    // 4. 迭代器的方法...
    public E next() {
        if (! hasNext())
            throw new NoSuchElementException();
        return (E) snapshot[cursor++];
    }
    
    //.... 可以发现的是,迭代器所有的操作都基于snapshot数组,而snapshot是传递进来的array数组
    

    缺点

    • 内存占用:如果 CopyOnWriteArrayList 经常要增删改里面的数组,经常执行 add(),set(),remove() 方法的话,是比较耗内存的,因为这几个操作都需要复制出一个数组。
    • 数据一致性:CopyOnWriteArrayList 容器只能保证最终一致性,不能保证数据的实时一致性。比如 A 线程在迭代,而此时 B 线程将部分数据修改了,但是 A 迭代的仍然是原有的数据。
  • 相关阅读:
    物料主档的屏幕增强
    删除请求号(网上转载)
    Echarts X轴内容过长自动隐藏,鼠标移动上去显示全部名称方法
    mscms学习
    java 基础(泛型)
    java 基础(枚举类)
    java 基础(抽象类)
    java 基础(单例)
    android项目杂记
    Android Studio--学习系列(3)发版
  • 原文地址:https://www.cnblogs.com/paulwang92115/p/12175753.html
Copyright © 2011-2022 走看看