Java進階:ArrayList線程安全問題詳解


概述

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();
        }
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM