ArrayList源码分析+ArrayList为什么不安全

持续更新中,未完坑。。。。

------------------------------------------------

先来Arraylist看看有什么属性

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始值大小.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 默认初始值的空数组
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * ArrayList基于此数组实现,默认大小为DEAFULT_CAPACITY也就是10
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList实际元素数量
     */
    private int size;

下面分析下add()函数源码


第一步:ensureCapacityInternal(size+1);是判断数组是否需要扩容,如果需要则扩容

第二步:elementData[size++] = e;直接进行赋值

先分析第一步,我们跟进去


1.先分析calculateCapacity()


先判断elementData数组是否为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那么这个很长的变量是啥呢?我们看一下


这里注释写的很清楚,可以简单理解为是一个空的数组。

就是说如果elementData为空则从 (DEFAULT_CAPACITY, minCapacity) 选择一个最大的然后返回。

(通过源码可以看到,这里的DEFAULT_CAPACITY默认大小为10

如果elementData不为空,则直接返回minCapacity。calculateCapacity()分析完毕。

2.下面分析ensureExplicitCapacity()


这里的modCount就是modified count,主要功能是记录每次修改的次数,不是本次分析重点,这里不详谈。

然后判断minCapacity是否大于当前数组的长度,如果大于,则进行扩容操作


3.这个grow()函数是扩容的核心函数,虽说是核心函数,但也不难理解。下面开始分析

private void grow(int minCapacity) {
    // 把数组长度赋值给oldCapacity
    int oldCapacity = elementData.length;
    // newCapacity等于旧的长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果newCapacity小于minCapacity则把minCapacity赋值给它。
    if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
    // 如果newCapacity大于MAX_ARRAY_SIZE则进入hugeCapcity(),
    if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);
    // 修改数组为新的长度,然后覆盖掉原先的数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

我们看一下这个MAX_ARRAY-SIZE是啥意思


简单说就是数组长度的最大值

但是这里为什么要减8呢?

注释中说一些虚拟机会保存header words在数组里。

分配更大的数组时可能会超过虚拟机的最大限制导致内存溢出。

但理解还是不清楚,所以Google了一下,在StackOverFlow找到了一些答案:https://stackoverflow.com/questions/35756277/why-the-maximum-array-size-of-arraylist-is-integer-max-value-8

有兴趣的可以自己去看,我摘取一段简单说就是数组本身需要8bytes的大小来存储数据,为了防止溢出,所以要减8。

4.然后分析hugeCapacity()


如果小于零,则抛出OutOfMemoryError()异常

如果minCapacity 大于 MAX_ARRAY_SIZE 则返回Integer.MAX_VALUE否则返回MAX_ARRAY_SIZE

5.最后通过copyOf函数把数组修改为新的大小然后覆盖掉以前的旧数组

第二步很简单

就是简单的赋值操作,但不是原子性,可以简单分为以下两步

elementData[size] = e;
size++;

至此add()函数分析完毕。

总结:

1.ArrayList底层是还是数组,只是增加了动态扩容的功能,因为底层是数组,所以有数组的特性:访问快,增加\删除效率低。

2.ArrayList是线程不安全的。

3.ArrayList每次扩容,为原来容量的1.5倍。

下面是一些常见问题:

1、为什么ArrayList是不安全的

先上一段示例代码

public static void main(String[] args) throws InterruptedException {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 100; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int j = 0; j < 100; j++) {
                    list.add(1);// 多执行几次会出现ArrayIndexOutOfBoundsException异常
                }
            }
        }).start();
    }
    Thread.sleep(100);// 等待子线程完全执行完毕
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i) == null) {
            System.out.println("i:" + i + ",数据:" + list.get(i));
        }
    }
    System.out.println("list.size():" + list.size());
}

运行结果如图:


这里有三个问题

1.为什么会出现ArrayIndexOutOfBoundsException的异常?

2.为什么有的数据为null?

3.为什么最后list.size()会少于理想中的情况?

下面开始分析

第一个问题:为什么会出现ArrayIndexOutOfBoundsException的异常?

上面已经说过,add()函数有两步,这个错误是由第一步ensureCapacityInternal(size+1);导致的

假如说当前size为9

1.线程A进来,读取size为9,而当前elementData大小为10,判断不需要扩容

2.这时候B也进来了,读取size也是为9,也是判断为不需要扩容

3.线程A开始进行设置值操作, elementData[9++] = e 操作,赋值成功,然后size变为10。

4.线程B也开始进行赋值操作,这时候size已经变为10,所以它尝试设置elementData[10] = e,但是elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException。

第二个问题:为什么有的数据为null?

这是由第二步elementData[size++] = e;导致的

第二步不是原子操作,可以大致分为以下两个操作

elementData[size] = e;
size++;

举个例子:假如说当前size为0

1.线程A读取到size为0,进行赋值为A

2.这时候线程B进来了,然后也进行赋值为B。

3.然后线程A对size+1,size=1

4.线程B对size+1,这时size=2

理想的情况应该是size[0]=A;size[1]=B。但此时size[0]=B;size[1]=null,而此时数组下标已经为2,所以下次添加元素时会从2开始,所以如果后期没有手动修改,size[1]始终为null。

第三个问题,为什么最后list.size()会少于理想中的情况?

因为在java中变量递增不是原子性的,所以size++本身也是线程不安全的

(Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write。)

这里的size++;其实可以分为三个独立的操作

1.读取size的值

2.将size值加1

3.然后将计算结果写回size。

递增的操作在多线程下就会出问题,举个例子

1.线程A读取size为1,将size值加1,然后把结果写回,这时size=2

2.这是线程B进来,读取size也是为1,然后size值加1,把结果写回,这时size还是2

所以导致递增失败,也就是会导致list的长度少于理想情况。

ArrayList, LinkedList, Vector的区别是什么?

  • ArrayList: 内部采用数组存储元素,支持高效随机访问,支持动态调整大小
  • LinkedList: 内部采用链表来存储元素,支持快速插入/删除元素,但不支持高效地随机访问
  • Vector: 可以看作线程安全版的ArrayList

猜你喜欢

转载自blog.csdn.net/crankz/article/details/80109748
今日推荐