一、概述
面試時相信面試官首先就會問到關于它的知識。一個經常被問到的問題就是: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,這也就是我們上面所說的情況。多測試幾次的話,數組越界的異常也可以復現出來。