CopyOnWriteArrayList源码解析

一、简介

CopyOnWriteArrayList通过读写分离的形式重构ArrayList,保证ArrayList在循环遍历过程中的读写分离性,保证数组的最终一致性,适用于多读少写的情景下。

二、继承体系

![CopyOnWrite继承体系](

)

CopyOnWriteArrayList实现了List,Serializable,RandomAccess,Cloneable接口

实现List接口,为提供add,remove,get,set等操作

实现RandomAcess接口表示该类可自由访问

和ArrayList的继承相对比发现COWA和其先辈实现的接口基本一致,其中最本质的是Collection和List,保证这两个类能够在轻易的进行切换(面向对象的多态性)。

1

三、重要属性

/** The lock protecting all mutators */
//可重入锁,实现并发控制
final transient ReentrantLock lock = new ReentrantLock();

/** The array, accessed only via getArray/setArray. */
//存储原始数组对象
private transient volatile Object[] array;

lock 实现写时加锁的控制

Object[] array存储需要的数据

四、重要方法解析

主要解析包括以下几个方法

4.1 构造函数

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)
            //如果数组类型不属于Object[].class 就重新赋值一份数组
            if (elements.getClass() != Object[].class)
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        setArray(elements);
    }

两个方式的拷贝:第一种方式是如果是相同的直接进行引用即可,第二种方式通过重新赋值数组并且进行数组元素类型的转化来实现。

4.2 eq方法

private static boolean eq(Object o1, Object o2) {
	return (o1 == null) ? o2 == null : o1.equals(o2);
}

用于判断两个对象是否相等,元素可能存在null,需要重写方法不能使用类似与o1.equals(o2)的方式会导致NPE。

4.2 set方法

/** 替换指定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) {//使用==而不是equals()来判断 
                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();//归还锁
        }
    }

主要流程:

  1. 上锁
  2. 获取index上的对象
  3. 查看两个是否相等
  4. 不相等则copy新的数组
  5. 修改新的数组并修改引用
  6. 解锁

set方法为写方法,需要继续进行加锁,实现多个写之间的一致性。
记录index上的元素oldValue,如果两个地址是不完全一致的就复制原有数组并进行元素的修改,最后复制回去。

为什么使用==进行比较而不是eq(A,B)
array中实际存储的是对象数组,array不同于hash需要使用hashcode和equals函数来进行判断

4.3 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;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

主要流程:

  1. 上锁
  2. 拷贝长度为len+1的新数组
  3. 进行复制
  4. 修改引用
  5. 解锁

从add中就能看出数组的长度变化情况,与ArrayList不同的扩容机制,COW的数组并不含有空余空间,数组完全饱和

4.4 add(int,E)方法

向指定位置上添加元素的方法

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];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

主要流程:

  1. 上锁
  2. 确定右侧需要移动的元素
    1. 需要移动的元素为0
      1. 直接进行扩容
    2. 需要移动的元素不为0
      1. 进行数组扩容
      2. 分左右两侧进行复制
  3. 替换位置上的值
  4. 解锁

4.5 addIfAbsent(E)方法

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :/*先进性快速的判断*/
    addIfAbsent(e, snapshot);
}

/**
     * A version of addIfAbsent using the strong hint that given
     * recent snapshot does not contain e.
     */
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))//判断公共的部分,如果该元素已经添加就不在添加
                    return false;
            if (indexOf(e, current, common, len) >= 0)//判断剩余的部分
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

主要流程:

  • 获取镜像进行,判断快照中是否存在元素e
    • 如果存在返回false表示添加失败
    • 如果不存在需要进行加锁操作
      • 上锁
      • 将现有元素与快照的公共部分(前半部分)进行比较,如果发现元素e已经被添加到数组中就结束
      • 后半部分进行indexOf查找操作,如果发现元素e就结束
      • 进行扩容并添加新数据到结尾
      • 解锁

其中有很多非常有意思的小细节
第一个:先进行了无锁化的查找,看是否存在元素,当不存在时再添加。而不是直接进行上锁在判断元素是否存在
第二个:正是由于上面的无锁化操作,导致快照和当前数组可能不一致,但依然利用上了快照信息,其中有个比较有意思的问题是没有直接使用indexOf进行重新查找,而是附加了比较查找,这里有个很底层的问题就是==!=的比较速度要比indexOf中的eq()的速度快得多,加之对于Array大部分的操作都是add或者add(index,e),如果index的值较大的话对于效率的提升会更加高

这里addIfAbsent(index,e)需要两步:确认是否存在和添加,将第一步的查找不加锁,而第二步修改进行加锁,实属精髓

posted @ 2019-09-20 20:36  随风而行-  阅读(306)  评论(0编辑  收藏  举报