暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

ArrayList,LinkedList,CopyOnWriteArrayList源码分析

盛超杰的笔记 2018-04-08
188

本文基于JDK1.8 在看dubbo的时候,看到使用了CopyOnWriteArrayList,顺带了解下

ArrayList

底层结构

ArrayList的底层是基于数组的

  1. transient Object[] elementData;

并且默认初始化的大小为0,可以从构造函数看到

  1. public ArrayList(int initialCapacity) {

  2.    if (initialCapacity > 0) {

  3.        this.elementData = new Object[initialCapacity];

  4.    } else if (initialCapacity == 0) {

  5.        this.elementData = EMPTY_ELEMENTDATA;

  6.    } else {

  7.        throw new IllegalArgumentException("Illegal Capacity: "+

  8.                                           initialCapacity);

  9.    }

  10. }



  11. public ArrayList() {

  12.    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;

  13. }


  14. public ArrayList(Collection<? extends E> c) {

  15.    elementData = c.toArray();

  16.    if ((size = elementData.length) != 0) {

  17.        // c.toArray might (incorrectly) not return Object[] (see 6260652)

  18.        if (elementData.getClass() != Object[].class)

  19.            elementData = Arrays.copyOf(elementData, size, Object[].class);

  20.    } else {

  21.        // replace with empty array.

  22.        this.elementData = EMPTY_ELEMENTDATA;

  23.    }

  24. }

在默认没有设置大小或者传入Collection为空的情况下,会设置elementData为EMPTYELEMENTDATA或者DEFAULTCAPACITYEMPTY_ELEMENTDATA,这两个常量都是长度为0数组

  1. private static final Object[] EMPTY_ELEMENTDATA = {};


  2. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

扩容机制

由于数组初始化长度是固定的,所以当个数超过一定限制时,会进行扩容操作,从add方法看起

  1. public boolean add(E e) {

  2.    ensureCapacityInternal(size + 1);  // Increments modCount!!

  3.    elementData[size++] = e;

  4.    return true;

  5. }

可以看到在对数组增加元素前,会调用ensureCapacityInternal方法进行扩容相关工作

  1. private void ensureCapacityInternal(int minCapacity) {

  2.    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {

  3.        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);

  4.    }


  5.    ensureExplicitCapacity(minCapacity);

  6. }

在ensureCapacityInternal方法中来确定扩容的最小值,在数组长度为0的情况下,使用默认数组大小,其他情况使用当前数组长度+1,

  1. private static final int DEFAULT_CAPACITY = 10;

然后在ensureExplicitCapacity会判断是否需要进行扩容

  1. private void ensureExplicitCapacity(int minCapacity) {

  2.    modCount++;


  3.    // overflow-conscious code

  4.    if (minCapacity - elementData.length > 0)

  5.        grow(minCapacity);

  6. }

只有最小扩容长度大于当前的数组长度时,才需要进行扩容,因为minCapacity是数组放入下一个元素后的长度,如果大于 elementData.length,说明当前数组已经放不下下一个数据,需要进行扩容 在grow方法中是具体的扩容逻辑

  1. private void grow(int minCapacity) {

  2.        // overflow-conscious code

  3.        int oldCapacity = elementData.length;

  4.        int newCapacity = oldCapacity + (oldCapacity >> 1);

  5.        if (newCapacity - minCapacity < 0)

  6.            newCapacity = minCapacity;

  7.        if (newCapacity - MAX_ARRAY_SIZE > 0)

  8.            newCapacity = hugeCapacity(minCapacity);

  9.        // minCapacity is usually close to size, so this is a win:

  10.        elementData = Arrays.copyOf(elementData, newCapacity);

  11.    }

对于初始化为0的数组,会直接扩容为10 对于长度不为0的数组,扩容大小为 oldCapacity + (oldCapacity >> 1),也就是变为原来的1.5倍 具体扩容的方式,使用Arrays.copyOf(elementData, newCapacity)进行深拷贝处理,新建一个数组,把老数组元素拷贝过去

  1. public static <T> T[] copyOf(T[] original, int newLength) {

  2.        return (T[]) copyOf(original, newLength, original.getClass());

  3.    }

  4. public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {

  5.        @SuppressWarnings("unchecked")

  6.        T[] copy = ((Object)newType == (Object)Object[].class)

  7.            ? (T[]) new Object[newLength]

  8.            : (T[]) Array.newInstance(newType.getComponentType(), newLength);

  9.        System.arraycopy(original, 0, copy, 0,

  10.                         Math.min(original.length, newLength));

  11.        return copy;

  12.    }

关于 System.arraycopy方法,是一个native方法

  1. public static native void arraycopy(Object src,  int  srcPos,

  2.                                        Object dest, int destPos,

  3.                                        int length);

如果是数组比较大,那么使用System.arraycopy会比较有优势,因为其使用的是内存复制,省去了大量的数组寻址访问等时间

SubList

可以调用subList方法来获取当前数组的子数组,对子数组的操作会影响到父数组,因为实际操作对象还是父数组,看下add方法

  1. public void add(int index, E e) {

  2.    rangeCheckForAdd(index);

  3.    checkForComodification();

  4.    parent.add(parentOffset + index, e);

  5.    this.modCount = parent.modCount;

  6.    this.size++;

  7. }

其实parent就指向了父数组

迭代器

在ArrayList中迭代器有2种,Itr和ListItr,分别通过iterator和listIterator拿到,ListItr继承于Itr 使用Itr只支持删除数组中的元素,不支持新增 使用ListItr支持新增,修改操作,但是新增的元素不会迭代出来

Itr的next方法返回的是原数组的引用

  1. public E next() {

  2.    checkForComodification();

  3.    int i = cursor;

  4.    if (i >= size)

  5.        throw new NoSuchElementException();

  6.    //使用的是原数组的引用

  7.    Object[] elementData = ArrayList.this.elementData;

  8.    if (i >= elementData.length)

  9.        throw new ConcurrentModificationException();

  10.    cursor = i + 1;

  11.    return (E) elementData[lastRet = i];

  12. }

因此对使用迭代器获取的元素进行修改时,原数组的元素也会被修改 但是在Itr迭代器使用过程中不能对数组内的元素个数进行修改,不然会抛出ConcurrentModificationException异常,主要通过modCount来实现 在得到迭代器的时候,会把当前数组的modCount存到expectedModCount变量中,并且涉及数组元素的修改操作都会对modCount++ 当使用next方法获取下一个元素的时候,会使用checkForComodification方法会对比两个modCount,如果不一致,说明数组被修改了,抛出异常

  1. final void checkForComodification() {

  2.    if (modCount != expectedModCount)

  3.        throw new ConcurrentModificationException();

  4. }

下面看下remove方法的实现

  1. public void remove() {

  2.    if (lastRet < 0)

  3.        throw new IllegalStateException();

  4.    checkForComodification();


  5.    try {

  6.        ArrayList.this.remove(lastRet);

  7.        cursor = lastRet;

  8.        lastRet = -1;

  9.        expectedModCount = modCount;

  10.    } catch (IndexOutOfBoundsException ex) {

  11.        throw new ConcurrentModificationException();

  12.    }

  13. }

lastRet保存的是当前元素的index,cursor保存的是下一个元素的index remove的时候因为把当前元素删除了,后面元素整体向前移动一位,所以cursor就等于lastRet了,并且要重置expectedModCount 继续看下ListItr的add方法

  1. public void add(E e) {

  2.    checkForComodification();


  3.    try {

  4.        int i = cursor;

  5.        ArrayList.this.add(i, e);

  6.        cursor = i + 1;

  7.        lastRet = -1;

  8.        expectedModCount = modCount;

  9.    } catch (IndexOutOfBoundsException ex) {

  10.        throw new ConcurrentModificationException();

  11.    }

  12. }

在增加元素后,会把cursor向右移动一位,并且重置expectedModCount

线程安全

ArrayList不是线程安全的,如果在多线程进行add的时候,发生了扩容,会发生不可知的错误吧,在多线程中使用ArrayList,需要进行同步

LinkedList

底层结构

LinkedList底层基于链表,会保留first,last 头尾指针,不存在扩容问题

  1. transient Node<E> first;


  2. transient Node<E> last;

Node节点定义如下

  1. private static class Node<E> {

  2.    E item;

  3.    Node<E> next;

  4.    Node<E> prev;


  5.    Node(Node<E> prev, E element, Node<E> next) {

  6.        this.item = element;

  7.        this.next = next;

  8.        this.prev = prev;

  9.    }

  10. }

是一个双向链表,所以LinkedList支持对头尾的操作,可以用来当栈,队列

查询优化

因为是链表,所以查询一个元素必须从头或尾开始查找,LinkedList会根据查找元素index进行优化

  1. Node<E> node(int index) {

  2.        // assert isElementIndex(index);


  3.        if (index < (size >> 1)) {

  4.            Node<E> x = first;

  5.            for (int i = 0; i < index; i++)

  6.                x = x.next;

  7.            return x;

  8.        } else {

  9.            Node<E> x = last;

  10.            for (int i = size - 1; i > index; i--)

  11.                x = x.prev;

  12.            return x;

  13.        }

  14.    }

size >> 1就是当前长度的中位数 如果在中位数左边,那么从头开始遍历 如果在中位数右边,从尾开始遍历 相比数组的随机访问,链表在查询方面是要慢一些,插入方面,也不一定链表占优,因为涉及到数组插入删除的位子,以及是否扩容问题。

CopyOnWriteArrayList

底层结构

CopyOnWriteArrayList底层基于数组,是一个线程安全的List,进行增删改的时候都会底层数组同步进行覆盖,相当于每次修改都会扩容,主要用于使用迭代器或者foreach的时候返回的是副本,提高遍历效率,但是修改操作性能降低。

修改操作

在修改,增加,删除操作都会使用ReentrantLock进行同步,我们以增加方法示例

  1. public boolean add(E e) {

  2.    final ReentrantLock lock = this.lock;

  3.    lock.lock();

  4.    try {

  5.        Object[] elements = getArray();

  6.        int len = elements.length;

  7.        Object[] newElements = Arrays.copyOf(elements, len + 1);

  8.        newElements[len] = e;

  9.        setArray(newElements);

  10.        return true;

  11.    } finally {

  12.        lock.unlock();

  13.    }

  14. }

在add方法中会调用Arrays.copyOf方法对原数组进行深拷贝,增加元素后,对原数组进行覆盖 其他修改操作也都会这样做,为什么?在修改操作把底层数组指向新的数组,但是迭代器里面引用的还是老数组,相当于每个迭代器都会存在特定时间点的一个副本,当然,对副本的操作,不会影响到新的底层数组

这个设计很巧妙,并没有弄两个数组来回切换,很好的利用了强引用。

看下CopyOnWriteArrayList中一个变量

  1. private final Object[] snapshot;

会把之前老数组的引用保存下来, 而在ArrayList或者LinkedList使用的都是从外部类拿直接引用,如

  1. Object[] elementData = ArrayList.this.elementData;

总结

ArrayList和LinkedList不是线程安全的,在多线程情况下需要进行同步, ArrayList底层数组,查询,修改快,插入,删除较慢 LinkedList底层链表插入,删除较快,查询,修改慢,同时可以模拟栈,队列操作 ArrayList和LinkedList不太好对比,和数据量,插入删除位置都有关,量级到一定程度在根据操作择优而选吧,一般使用ArrayList 而CopyOnWriteArrayList是线程安全的,但是也要考虑场景,查询以及遍历是多数操作的情况下适合使用,毕竟每次修改都需要深拷贝,数据量大了也很损耗性能,并且也浪费内存,可能还不如同步的ArrayList


文章转载自盛超杰的笔记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论