博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从面试角度分析CopyOnWriteArrayList源码
阅读量:4102 次
发布时间:2019-05-25

本文共 4041 字,大约阅读时间需要 13 分钟。

点击上方 Java旅途,选择 设为星标

优质文章,每日送达


注:本系列文章中用到的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集合效率更高。

推荐阅读

编程·思维·职场欢迎扫码关注

转载地址:http://ceysi.baihongyu.com/

你可能感兴趣的文章
HTML&CSS进阶
查看>>
Servlet进阶和JSP基础
查看>>
servlet&jsp 的使用以及jsp的历史遗留用法
查看>>
servlet中的cookie和session
查看>>
过滤器及JSP九大隐式对象
查看>>
软件(项目)的分层
查看>>
菜单树
查看>>
MySQL-分布式架构-MyCAT
查看>>
设计模式六大原则(6):开闭原则
查看>>
阿里面试总结--JAVA
查看>>
Servlet的生命周期
查看>>
JAVA八大经典书籍,你看过几本?
查看>>
《读书笔记》—–书单推荐
查看>>
【设计模式】—-(2)工厂方法模式(创建型)
查看>>
有return的情况下try catch finally的执行顺序(最有说服力的总结)
查看>>
String s1 = new String("abc"); String s2 = ("abc");
查看>>
JAVA数据类型
查看>>
Xshell 4 入门
查看>>
SoapUI-入门
查看>>
Oracle -常用命令
查看>>