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,這也就是我們上面所說的情況。多測試幾次的話,數組越界的異常也可以復現出來。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容