概述

ArrayList 是线程不安全的集合类,当多线程环境下,并发对同一个ArrayList执行add,可能会抛出java.util.ConcurrentModificationException的异常

例子

这边有个简单的程序,创建30个线程,分别对ArrayList执行add操作

public class ListApp
{
    public static void main( String[] args ) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 1; i <= 30; i++){
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }).start();
        };
    }
}

输出结果如下,确实报错了

在这里插入图片描述

异常原因分析

首先,看一下 ArrayList 源码,这里只贴出代码关键的部分。里面的注释是我自己添加的,这样看起来更清晰一些。

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //ArrayList的底层存储就是个Object[]数组
    transient Object[] elementData;
    private int size;
    private static final int DEFAULT_CAPACITY = 10;

    public ArrayList() {
    	//构造函数将elementData初始化为{}空数组
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public boolean add(E e) {
    	//检查elementData数组大小,大小不够就进行数组扩容。继续跳到下一个函数
        ensureCapacityInternal(size + 1);
        
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
    	//如果当前是空数组,就把数组大小初始化为10(DEFAULT_CAPACITY=10)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

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

        //数组大小不够存放新数据了,此时需要扩容
        if (minCapacity - elementData.length > 0)
        	//grow()才是真正执行扩容的函数。继续往下看
            grow(minCapacity);
    }

    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
            
        //这里就不细讲了,关键就是计算出newCapacity新的容器大小
        //调用Arrays.copyOf进行复制,构造出一个新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

异常原因总结

由此可见,ArrayList的所有方法都没有加Lock,也没有加synchronized,因此在并发操作下,扩容函数grow()会存在问题。

举个简单的例子:

  • elementData数组刚刚添加了最后一个元素,也就是刚好满员了
  • 这时2个线程同时又调用了add,那么就必须要执行grow进行扩容
  • 第1个线程调用完grow(),然后也调用了elementData[size++] = e,把新元素添加上去
  • 第2个线程又调用一次grow(),整个elementData数组就乱掉了

问题解决

使用 Vector 初始化 list 对象

List<String> list = new Vector<>();

因为Vector.add使用了synchronized加锁

    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

使用 Collections.synchronizedList

List<String> list = Collections.synchronizedList(new ArrayList<>());

这种情况下,调用的是SynchronizedList.add,源码如下,同样做了加锁

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}

使用 CopyOnWriteArrayList

List<String> list = new CopyOnWriteArrayList<>();

底层使用的是ReentrantLock,源码如下:

    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();
        }
    }
posted on 2020-09-17 22:41  风停了,雨来了  阅读(2421)  评论(0编辑  收藏  举报