从CopyOnWriteArrayList谈等效不可变对象在源码中的应用

1 从CopyOnWriteArrayList谈等效不可变对象在源码中的应用

CopyOnWriteArrayList的源码中应用了等效不可变对象。使得集合在遍历操作的时候,不用加锁也能保证线程安全。

1.1 CopyOnWriteArrayList Source Code

    public class CopyOnWriteArrayList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
            private static final long serialVersionUID = ... ;
            
            final transient ReentrantLock lock = new ReentrantLock();
            
            private transient volatile Object[] array;
            
            final Object[] getArray() {return array;}

            final void setArray(Object[] a) {array = a;}
        }

可以看到CopyOnWriteArrayList源码中,维护了一个array对象数组,用于储存集合中每一个元素,并且这个array数组,只能够通过get和set方法来访问。

    // 遍历CopyOnWriteArrayList方法
    public Iterator<E> iterator(){
        return new COWIterator<E>(getArray(), 0);
    }
    // 新增一个元素的方法
    public boolean add(E e){
        final ReentrantLock lock = this.lock;
        lock.lock();
        try{
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elementes, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

增加元素的步骤:

  1. elements = 当前数组
  2. len = 当前数组长度
  3. 新建一个 newElements,把elements复制进去,并且长度增加1
  4. 把增加的元素放在newElements最后
  5. 调取setArray方法,将newElements数组更新旧的数组。
  6. 返回true,解锁。

1.2 分析关键点

  • array被创建后,不会被修改。包括长度和内容。
  • 增删时,先整体复制array到newElements中,在新的数组中做好操作,直接整体替换旧数组。
  • 在操作新数组时,旧数组内的数值至始至终不改变。

实例Array本质上就是一个数组,数组内的元素是对象,每个对象的内部状态可以发生替换,因此这并非是严格的不可变对象,所以称之为等效不可变对象

1.3 继续说明

下面演示上述所谓的非严格的不可变对象的情况

    public static void main(String[] args){
        List<Message> list = new CopyOnWriteArrayList<>();
        Message message = new Message();
        message.setMsg("aaa");
        list.add(message);
        message.setMsg("bbb");
    }

照理来说,放在CopeOnWriteArrayList中的东西应该不能动,除非整个替换。但是我们可以通过调用存在数组中的对象的set方法,修改了对象数值。

这就是不严格的不可变对象。 称之为:等效不可变对象

1.4 精髓

这个数组是写时复制,也就是当要更新数组内容时,先复制一个副本,在副本上做修改,然后用副本替换原本。

写时复制的意义在于,当读多写少的场景时,通过这个机制,大量的读请求在无需加锁牺牲性能的情况下,保证多线程的并发读写安全。

这个数组的弱点在于:弱一致性

因为任何线程从原理上,只不过是获得了一个数组备份的副本而已,当原本数组发生改变时,这种改变,并不会体现在副本上。

1.5 实际应用 in MySQL Driver

在mysql driver中,有一个registerDriver方法,该方法用来保存不同的数据库驱动的。

源码省略。

无论有多少个数据库模型,数据库的驱动程序一般都是在程序启动的时候加载的。也就是说,registerDriver方法,一般来说都是在程序启动的时候进行调用的,在程序运行的过程中,一般不会调用这个方法,所以非常适合这个List

posted @ 2021-06-10 13:37  Yiyang_Cai  阅读(70)  评论(0编辑  收藏  举报