为何线程安全的List推荐使用CopyOnWriteArrayList,而不是Vector

注:本系列文章中用到的jdk版本均为java8java

相比不少同窗在刚接触Java集合的时候,线程安全的List用的必定是Vector。可是如今用到的线程安全的List通常都会用CopyOnWriteArrayList,不多有人再去用Vector了,至于为何,文章中会具体说到。接下来,咱们先来简单分析如下Vector的源码。面试

1、Vector集合源码简析

因为本文的重点不是Vector集合,所以只是简单的分析一下Vector的初始化方法和添加元素的方法。数组

Vector的底层实现和ArrayList同样,都是由数组实现的。安全

Vector的主要变量以下:微信

/** * 存放元素的数组 */
protected Object[] elementData;
/** * 元素个数 */
protected int elementCount;
/** * 扩容自增容量大小 */
protected int capacityIncrement;
复制代码

1.1 Vector初始化

Vector的初始化提供了三个方法,除了能够指定初始容量的大小,还能够指定扩容容量的大小。构造器分别以下:markdown

无参构造器多线程

public Vector() {
    this(10);
}
复制代码

指定初始化容量的构造器dom

public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
复制代码

指定初始化容量和扩容容量大小的构造器oop

public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}
复制代码

从上面的构造器中能够看出,若是调用无参构造器,则会建立一个初始化容量为10,扩容容量为0Vector集合。性能

1.2 如何扩容

Vector的扩容机制和ArrayList的很像,若是不清楚ArrayList的扩容机制,能够看看这篇文章。这里咱们直接看Vector的扩容方法grow

private void grow(int minCapacity) {
    // overflow-conscious code
    // 初始化数组的长度,默认为10
    int oldCapacity = elementData.length;
    // 是否指定扩容容量,不指定扩容为原来的2倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}
复制代码

经过上面的方法,咱们能够看出,若是指定了扩容容量的大小则扩容的新数组大小为原来的数组加上扩容容量的大小,若是不指定扩容容量的大小则扩容的新数组大小为原来数组大小的2倍。这样扩容为原来的2倍是很消耗空间的,这也是Vector被弃用的缘由之一。

除此以外,看过源码的同窗可能发现了,Vector集合的全部操做元素的方法都加了synchronized关键字,这就致使了操做Vector的效率会很是低,在开发中,每每读操做的使用频率会远高于其余操做,而CopyOnWriteArrayList 就是这样一种读操做效率远高于写操做效率的List,一块儿来看看。

2、CopyOnWriteArrayList源码简析

CopyOnWriteArrayList 类图:

2.1 CopyOnWrite思想

CopyOnWrite简称COW,根据名字来看就是写入时复制。意思就是你们共同去访问一个资源,若是有人想要去修改这个资源的时候,就须要复制一个副本,去修改这个副本,而对于其余人来讲访问得资源仍是原来的,不会发生变化。

2.2 初始化CopyOnWriteArrayList

CopyOnWriteArrayList 底层是也是有数组实现的。 本文咱们只解读添加元素和读取元素的区别,删除修改元素原理和添加元素差很少,操做时都须要进行加锁,而读操做不会加锁。

CopyOnWriteArrayList 主要有如下两个变量:

// 独占锁
final transient ReentrantLock lock = new ReentrantLock();

// 存放元素的数组
private transient volatile Object[] array;
复制代码

咱们仔细来分析一下上面两个属性,这两个思想是 CopyOnWriteArrayList 的核心 。

  • lock:ReentrantLock,独占锁,多线程运行的状况下,只有一个线程会得到这个锁,只有释放锁后其余线程才能得到。
  • array:存放数据的数组,关键是被volatile修饰了,被volatile修饰,就保证了可见性,也就是一个线程修改后,其余线程当即可见。

最经常使用的初始化方式以下:

/** * Creates an empty list. */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

/** * Sets the array. */
final void setArray(Object[] a) {
    array = a;
}
复制代码

初始化只是建立了一个空的数组,并将array指向它。

2.3 添加元素

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取原来的数组
        Object[] elements = getArray();
        // 原来数组的长度
        int len = elements.length;
        // 建立一个长度+1的新数组,并将原来数组的元素复制给新数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 元素放在新数组末尾
        newElements[len] = e;
        // array指向新数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
复制代码

添加数组的步骤以下:

  1. 得到独占锁,将添加功能加锁
  2. 获取原来的数组,并获得其长度
  3. 建立一个长度为原来数组长度+1的数组,并拷贝原来的元素给新数组
  4. 追加元素到新数组末尾
  5. 指向新数组
  6. 释放锁

这个过程是线程安全的,COW的核心思想就是每次修改的时候拷贝一个新的资源去修改,add()方法再拷贝新资源的时候将数组容量+1,这样虽然每次添加元素都会浪费必定的空间,可是数组的长度正好是元素的长度,也在必定程度上节省了扩容的开销。

2.4 获取元素

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

final Object[] getArray() {
    return array;
}

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

读操做是自然安全的操做,并且数组自己会进行检查越界问题,所以获取元素的方法很简单,只是根据索引获取该元素。

public int size() {
    return getArray().length;
}
复制代码

因为CopyOnWriteArrayList的底层数组长度,自己就是元素大小,所以size()方法只要返回数组长度就能够了。

3、总结

VectorCopyOnWriteArrayList都是线程安全的List,底层都是数组实现的,Vector的每一个方法都进行了加锁,而CopyOnWriteArrayList的读操做是不加锁的,所以CopyOnWriteArrayList的读性能远高于VectorVector每次扩容的大小都是原来数组大小的2倍,而CopyOnWriteArrayList不须要扩容,经过COW思想就能使数组容量知足要求。两个集合都是先了RandomAccess接口,支持随机读取,所以更加推荐使用for循环进行遍历。在开发中,读操做会远远多于其余操做,所以使用CopyOnWriteArrayList集合效率更高。

点关注、不迷路

若是以为文章不错,欢迎关注、点赞、收藏,大家的支持是我创做的动力,感谢你们。

若是文章写的有问题,请不要吝惜文笔,欢迎留言指出,我会及时核查修改。

若是你还想看到更多别的东西,能够微信搜索「Java旅途」进行关注。「Java旅途」目前已经整理各类中间件的使用教程及各种Java相关的面试题。扫描下方二维码进行关注就能够获得这些资料。

相关文章
相关标签/搜索