乐趣区

关于java:为什么线程安全的List推荐使用CopyOnWriteArrayList而不是Vector

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

相比很多同学在刚接触 Java 汇合的时候,线程平安的 List 用的肯定是 Vector。然而当初用到的线程平安的 List 个别都会用CopyOnWriteArrayList,很少有人再去用Vector 了,至于为什么,文章中会具体说到。接下来,咱们先来简略剖析以下 Vector 的源码。

一、Vector 汇合源码简析

因为本文的重点不是 Vector 汇合,因而只是简略的剖析一下 Vector 的初始化办法和增加元素的办法。

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

Vector的次要变量如下:

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

1.1 Vector 初始化

Vector的初始化提供了三个办法,除了能够指定初始容量的大小,还能够指定扩容容量的大小。结构器别离如下:

无参结构器

public Vector() {this(10);
}

指定初始化容量的结构器

public Vector(int initialCapacity) {this(initialCapacity, 0);
}

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

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,一起来看看。

二、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() 办法只有返回数组长度就能够了。

三、总结

VectorCopyOnWriteArrayList 都是线程平安的 List,底层都是数组实现的,Vector的每个办法都进行了加锁,而 CopyOnWriteArrayList 的读操作是不加锁的,因而 CopyOnWriteArrayList 的读性能远高于 VectorVector 每次扩容的大小都是原来数组大小的 2 倍,而 CopyOnWriteArrayList 不须要扩容,通过 COW 思维就能使数组容量满足要求。两个汇合都是先了 RandomAccess 接口,反对随机读取,因而更加举荐应用 for 循环进行遍历。在开发中,读操作会远远多于其余操作,因而应用 CopyOnWriteArrayList 汇合效率更高。

点关注、不迷路

如果感觉文章不错,欢送关注、点赞、珍藏,你们的反对是我创作的能源,感激大家。

如果文章写的有问题,请不要吝惜文笔,欢送留言指出,我会及时核查批改。

如果你还想看到更多别的货色,能够微信搜寻「Java 旅途」进行关注。「Java 旅途」目前曾经整顿各种中间件的应用教程及各类 Java 相干的面试题。扫描下方二维码进行关注就能够失去这些材料。

退出移动版