爲什麼說ArrayList是線程不安全的

一.概述
對於ArrayList,相信大家並不陌生。這個類是我們平時接觸得最多的一個列表集合類。

面試時相信面試官首先就會問到關於它的知識。一個經常被問到的問題就是:ArrayList是否是線程安全的?

答案當然很簡單,無論是背來的還是自己看過源碼,我們都知道它是線程不安全的。那麼它爲什麼是線程不安全的呢?它線程不安全的具體體現又是怎樣的呢?我們從源碼的角度來看下。

二.源碼分析
首先看看這個類所擁有的部分屬性字段:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     * 列表元素集合數組
     * 如果新建ArrayList對象時沒有指定大小,那麼會將EMPTY_ELEMENTDATA賦值給elementData,
     * 並在第一次添加元素時,將列表容量設置爲DEFAULT_CAPACITY 
     */
    transient Object[] elementData; 

    /**
     * 列表大小,elementData中存儲的元素個數
     */
    private int size;
}

所以通過這兩個字段我們可以看出,ArrayList的實現主要就是用了一個Object的數組,用來保存所有的元素,以及一個size變量用來保存當前數組中已經添加了多少元素。

接着我們看下最重要的add操作時的源代碼:

public boolean add(E e) {

    /**
     * 添加一個元素時,做了如下兩步操作
     * 1.判斷列表的capacity容量是否足夠,是否需要擴容
     * 2.真正將元素放在列表的元素數組裏面
     */
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

ensureCapacityInternal()這個方法的詳細代碼我們可以暫時不看,它的作用就是判斷如果將當前的新元素加到列表後面,列表的elementData數組的大小是否滿足,如果size + 1的這個需求長度大於了elementData這個數組的長度,那麼就要對這個數組進行擴容。

由此看到add元素時,實際做了兩個大的步驟:

判斷elementData數組容量是否滿足需求
在elementData對應位置上設置值
這樣也就出現了第一個導致線程不安全的隱患,在多個線程進行add操作時可能會導致elementData數組越界。具體邏輯如下:

列表大小爲9,即size=9
線程A開始進入add方法,這時它獲取到size的值爲9,調用ensureCapacityInternal方法進行容量判斷。
線程B此時也進入add方法,它獲取到size的值也爲9,也開始調用ensureCapacityInternal方法。
線程A發現需求大小爲10,而elementData的大小就爲10,可以容納。於是它不再擴容,返回。
線程B也發現需求大小爲10,也可以容納,返回。
線程A開始進行設置值操作, elementData[size++] = e 操作。此時size變爲10。
線程B也開始進行設置值操作,它嘗試設置elementData[10] = e,而elementData沒有進行過擴容,它的下標最大爲9。於是此時會報出一個數組越界的異常ArrayIndexOutOfBoundsException.
另外第二步 elementData[size++] = e 設置值的操作同樣會導致線程不安全。從這兒可以看出,這步操作也不是一個原子操作,它由如下兩步操作構成:

elementData[size] = e;
size = size + 1;
在單線程執行這兩條代碼時沒有任何問題,但是當多線程環境下執行時,可能就會發生一個線程的值覆蓋另一個線程添加的值,具體邏輯如下:

列表大小爲0,即size=0
線程A開始添加一個元素,值爲A。此時它執行第一條操作,將A放在了elementData下標爲0的位置上。
接着線程B剛好也要開始添加一個值爲B的元素,且走到了第一步操作。此時線程B獲取到size的值依然爲0,於是它將B也放在了elementData下標爲0的位置上。
線程A開始將size的值增加爲1
線程B開始將size的值增加爲2
這樣線程AB執行完畢後,理想中情況爲size爲2,elementData下標0的位置爲A,下標1的位置爲B。而實際情況變成了size爲2,elementData下標爲0的位置變成了B,下標1的位置上什麼都沒有。並且後續除非使用set方法修改此位置的值,否則將一直爲null,因爲size爲2,添加元素時會從下標爲2的位置上開始。

接下來我們用個小例子驗證一下。

三.案例復現
我們用如下的代碼可以進行安全性的校驗:

public static void main(String[] args) throws InterruptedException {
    final List<Integer> list = new ArrayList<Integer>();

    // 線程A將0-1000添加到list
    new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < 1000 ; i++) {
                list.add(i);

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    // 線程B將1000-2000添加到列表
    new Thread(new Runnable() {
        public void run() {
            for (int i = 1000; i < 2000 ; i++) {
                list.add(i);

                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();

    Thread.sleep(1000);

    // 打印所有結果
    for (int i = 0; i < list.size(); i++) {
        System.out.println("第" + (i + 1) + "個元素爲:" + list.get(i));
    }
}

最後的輸出結果中,有如下的部分:

第7個元素爲:3
第8個元素爲:1003
第9個元素爲:4
第10個元素爲:1004
第11個元素爲:null
第12個元素爲:1005
第13個元素爲:6

可以看到第11個元素的值爲null,這也就是我們上面所說的情況。

多測試幾次的話,數組越界的異常也可以復現出來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章