ArrayList在多线程下出现的问题

1,567 阅读4分钟

1、add(E e)源码

首先看一下jdk 1.8下ArrayList的add(E e)方法的实现:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

接下来,我们细致分析一下,该方法每行的代码:

1.1 ensureCapacityInternal(int minCapacity)方法

源码为:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

该方法的目的就是扩容原来的Object[] elementDate数组(ps:ArrayList底层就是用Object[]数组实现的),而传入的参数int minCapacity指的是扩容之后的Object[]数组size的大小。

细致看一下calculateCapacity(Object[] elementData, int minCapacity)方法和ensureExplicitCapacity(int minCapacity)方法:

1.1.1 calculateCapacity(Object[] elementData, int minCapacity)方法

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA源码为:

/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

DEFAULT_CAPACITY为:

/**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

这里也说明了,ArrayList不是刚开始就初始化容量为10,而是直到添加第一个元素的时候,才会讲容量变成10。

1.1.2 ensureExplicitCapacity(int minCapacity)方法

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

modCount 表示该 ArrayList 被更改过多少次(倘若在用迭代器遍历ArrayList的时候,更改了容器ArrayList会报错,和这个有关系)。

当传入的参数minCapacity(如前文所述,这里传入的minCapacity一般都是扩容后的大小)比原ArrayList的容量大的时候,则会扩容。(特殊情况是,扩容后的容量已经超过了int的最大值,则不会扩容)。

特别的,看一下扩容方法:

1.1.2.1 grow(int minCapacity)——扩容方法
/**
 * Increases the capacity to ensure that it can hold at least the
 * number of elements specified by the minimum capacity argument.
 *
 * @param minCapacity the desired minimum capacity
 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

很明显,一般而言,扩容后的容量一般是原始容量的1.5倍。但是这里源码判断了if (newCapacity - minCapacity < 0)的情况,那这里为什么要这么判断呢?难道扩容完新容量还会变小吗?答案是存在这种情况的:(参考文章:blog.csdn.net/anglehuap/a… int oldCapacity = Integer.MAX_VALUE-11111111;,根据扩容算法得到int newCapacity = oldCapacity + (oldCapacity >> 1);。输出结果为oldCapacity = 2136372536,而newCapacity = -1090408492,这时候我们新容量等于原来的容量,相当于不扩容。

再看一下if (newCapacity - MAX_ARRAY_SIZE > 0)这个判断,首先看一下MAX_ARRAY_SIZE是什么。其源码为:

/**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

至于为什么是Integer.MAX_VALUE - 8,是因为虚拟机会给数组这个大对象分配对象头,一般而言是8个字节,防止OOM的。而hugeCapacity(int minCapacity)则确保了int边界的情况,源码为:

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

2、分析add(E e)方法

很明显,添加的方法为:

ArrayList的底层是一个动态数组,ArrayList首先会对传进来的初始化参数initalCapacity进行判断,如果参数等于0,则将数组初始化为一个空数组,如果不等于0,将数组初始化为一个容量为10的数组。初始容量也可以自定义指定。随着不断添加元素,数组大小增加,当数组的大小大于初始容量的时候(比如初始为10,当添加第11个元素的时候),就会进行扩容,新的容量为旧的容量的1.5倍。扩容的时候,会以新的容量建一个原数组的拷贝,修改原数组,指向这个新数组,原数组被抛弃,会被GC回收。

(参考:blog.csdn.net/FateRuler/a…

3、多线程下的情况

假如原始数组的容量为5,索引为4。线程A刚运行完add方法的第一行,即ensureCapacityInternal(size + 1);,此时size + 1了,但是线程A并没有分配值到size + 1这个位置上时间片就结束了。此时运行线程B,线程B完整地跑完了add方法,此时线程A再运行的时候,则会将线程B在索引为5的地方写的值覆盖掉。

4、改进

使用线程安全的CopyOnWriteArrayList类去完成多线程下的工作,这个类在读多写少的情况下,性能很好,因为只在写的时候(即在使用add(E e)方法)才会用ReentrantLock锁,读则是直接读。 直接贴一下CopyOnWriteArrayListadd(E e)方法的源码:

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    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();
    }
}