JDK源码阅读(十) : 并发容器——CopyOnWriteArrayList

1. 同步容器和并发容器

Java1.5以前,提供线程安全的容器是同步容器,比如Vector、Stack 和 Hashtable,这些容器使用synchronized关键字修饰方法,以保证互斥。这种方式的串行度太高,所以效率很低。

Java1.5开始,提供了性能更好的并发容器。并发容器也可分成四大类:List、Map、Set和Queue。

2. CopyOnWriteArrayList

CopyOnWriteArrayList是ArrayList的线程安全版本,从名称上来看,它是写时复制的ArrayList。在java1.5,由并发大师Doug Lea所设计。该类可以存储元素null。

2.1 继承关系

CopyOnWriteArrayList并没有继承ArrayList,也没有继承AbstractList,而只是实现了List接口,因为它的实现方式与ArrayList不太相同。

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

2.2 成员属性

(1)一把可重入的锁,用于保证写操作时的线程同步。因为锁不可被修改,所以使用final关键字进行修饰。

final transient ReentrantLock lock = new ReentrantLock();

(2)Object数组用于存放容器中的元素,使用volatile关键字修饰,禁用缓存,防止出现线程安全问题。

private transient volatile Object[] array;

2.3 构造器

(1)空参构造器,仅创造了一个长度为0的Object数组。

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
final void setArray(Object[] a) {
    array = a;
}

(2)将容器c中的所有元素都添加到新容器的构造器,添加顺序按照容器c的迭代顺序。

public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
  	//如果容器c的类型正好是CopyOnWriteArrayList,则可以直接取其内部的数组,作为新容器的内部数组
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
      	//否则,调用该容器类自身的toArray方法,该方法通常会复制一份容器内部的数组,然后返回副本
        elements = c.toArray();
      	//如果c的类型不是ArrayList,那么其持有的数组可能不是Object类型的,
      	//需要将该数组中的元素全部复制到一个新的Object数组,再引用新数组。
        if (c.getClass() != java.util.ArrayList.class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    setArray(elements);
}

(3)构造器传入一个E类型的数组,E是CopyOnWriteArrayList类的泛型参数,将该数组元素全部复制到一个新建的Object数组中,然后容器内部引用这个新数组。

public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

2.4 添加元素

(1)add(E)方法添加一个元素到容器中。

对于容器的写操作,CopyOnWriteArrayList执行写时复制策略。首先会把原数组的内容全部复制到新数组中,再把要添加到元素写入新数组。最后,让本容器引用新数组。

写操作是加锁的,保证了同一时刻只有一个线程执行写操作,防止多个线程同时创建出多个副本。

public boolean add(E e) {
  	//设置一把锁,这把锁是本类中的成员对象lock
    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();
    }
}

可以看到,CopyOnWriteArrayList执行写操作的成本非常高,每个写操作不仅要互斥执行,还要创建一个副本

(2)add(int index, E element)方法在数组的给定位置上插入一个元素。

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;
      	//如果是在原数组尾部添加,则不需要移动任何元素,只需要创建一个长度加1的数组
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);
        else {
          	//创建一个长度加1的数组
            newElements = new Object[len + 1];
          	//拷贝0到index-1的元素到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
          	//拷贝index及后面的元素到新数组中,从新数组的index+1的位置开始放
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
      	//将新元素放入新数组的index位置,再引用新数组
        newElements[index] = element;
        setArray(newElements);
    } finally {
        lock.unlock();
    }
}

2.5 查找元素

CopyOnWriteArrayList的读操作不使用互斥锁,因为读操作不会改变容器的内容。

(1)contains(Object o)方法判断容器中是否包含某个元素

public boolean contains(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length) >= 0;
}

indexOf方法从参数的index方法开始查找,当到达参数fence位置时,停止查找,查找范围不包括fence位置。找到则会返回该元素的索引,找不到会返回-1。

private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
  	//因为容器中可以存放null,而null可能会引发空指针异常,所以要单独判断
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    return -1;
}

(2)indexOf(Object o)方法查找某个元素的第一个索引位置。

public int indexOf(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length);
}

(3)lastIndexOf(Object o)方法查找某个元素的最后一个索引位置。其实就是从后往前查找第一个相同的元素。

public int lastIndexOf(Object o) {
    Object[] elements = getArray();
    return lastIndexOf(o, elements, elements.length - 1);
}

(4)get(int index)方法获取某个索引位置的元素。

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

2.6 删除元素

(1)remove(int index)方法删除给定位置上的元素。

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 {
          	//否则,需要将index后面的元素前移一位
            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();
    }
}

(2)remove(Object o)方法删除给定元素第一次出现的位置上的元素。

public boolean remove(Object o) {
  	//取得当前时刻容器内的数组,作为快照snapshot
    Object[] snapshot = getArray();
  	//获取该元素在快照中的位置
    int index = indexOf(o, snapshot, 0, snapshot.length);
  	//快照中没有,就不用删
    return (index < 0) ? false : remove(o, snapshot, index);
}

真正执行删除操作时,要加锁。

由于在取得元素在快照中的位置开始,到真正删除元素前,由于没有加锁,这期间容器可能发生写操作。

如果发生了写操作,内部持有的数组会改变,不同于之前拍下的快照,此时要重新查找要删除的元素位置。

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);
          	//找index或者len前面是否有元素被改为了要删除的元素,如果找到,那个位置的元素就是现在要删除的     
            for (int i = 0; i < prefix; i++) {
              	//为什么做以下判断?原因如下:
              	//如果当前位置的指针没有发生过变化,则肯定不是要删除的,可以直接跳过equals验证
       //只有当前位置的指针发生了变化,才可能存在当前位置变成了要删除的元素,然后再进一步验证是否真的要删除
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
						//如果index依然超出len,则没有要删除的元素
            if (index >= len)
                return false;
          	//如果index<len,并且找到了,则可以退出代码块
            if (current[index] == o)
                break findIndex;
          	//如果没找到,就要从index开始往后找
            index = indexOf(o, current, index, len);
          	//还是没找到,返回false
            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();
    }
}

(3)removeAll(Collection<?> c)方法删除容器c中的所有元素。

public boolean removeAll(Collection<?> c) {
    if (c == null) throw new NullPointerException();
    final ReentrantLock lock = this.lock;
  	//上锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        if (len != 0) {
            // 创建一个临时数组,保存不用删除的元素
            int newlen = 0;
            Object[] temp = new Object[len];
            for (int i = 0; i < len; ++i) {
                Object element = elements[i];
              	//如果当前元素不是c中的元素,则保留
                if (!c.contains(element))
                    temp[newlen++] = element;
            }
          	//如果存在要删除的元素,则将临时数组temp拷贝一份,本容器的数组引用该副本
            if (newlen != len) {
                setArray(Arrays.copyOf(temp, newlen));
                return true;
            }
        }
        return false;
    } finally {
        lock.unlock();
    }
}

2.7 修改元素

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 {
            // 否则,不作修改
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

2.8 迭代器

iterator() 方法获取COWIterator迭代器,将当前时刻的数组和起始遍历位置作为参数传入。

public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}

COWIterator类存储了获取迭代器实例时,容器中持有的数组快照,以及下一个要返回的元素位置。

static final class COWIterator<E> implements ListIterator<E> {
    /** Snapshot of the array */
    private final Object[] snapshot;
    /** Index of element to be returned by subsequent call to next.  */
    private int cursor;

构造器初始化快照snapshot和下一个要返回的元素位置cursor。迭代器中所有的操作都是在快照上进行的。

private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}

hasNext() 方法。cursor小于数组快照的长度时,存在下一个要返回的元素。

public boolean hasNext() {
    return cursor < snapshot.length;
}

hasPrevious()方法。cursor大于0时,存在前项元素。这里的前项元素就是指next-1位置上的元素。

public boolean hasPrevious() {
    return cursor > 0;
}

next()方法获取下一个位置的元素。

public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
  	//游标后移一位
    return (E) snapshot[cursor++];
}

注意:CopyOnWriteArrayList的迭代器不支持写操作。可以看到如果执行写操作,会直接抛异常。

public void remove() {
    throw new UnsupportedOperationException();
}

public void set(E e) {
    throw new UnsupportedOperationException();
}

public void add(E e) {
    throw new UnsupportedOperationException();
}

由于迭代器的所有操作都是在快照上进行的,写操作在副本上执行,不影响快照的内容。

因此,CopyOnWriteArrayList不会触发fast-fail事件,其他的同步容器也是如此。

同时,由于迭代器遍历的旧版本的数组,不会及时读到新的改变。

2.9 小结

①CopyOnWriteArrayList对读操作不加锁,写操作会加锁

执行写操作时,不在原数组上进行操作,而是创建一个数组副本,在副本上执行写操作。执行完毕后,引用新数组。

因此,CopyOnWriteArrayList适用于读多写少的场景,因为它执行写操作的开销太大。

②迭代器只做读操作,并且是在迭代器实例创建时所获取的数组快照上进行遍历。写操作不会影响这个快照,所以CopyOnWriteArrayList不会触发fast-fail机制

由于迭代器是在旧快照上遍历,不能及时读到容器的新变化。因此,CopyOnWriteArrayList适用于能够容忍读写短暂不一致的场景

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值