多執行緒場景下使用 ArrayList,要注意這幾點!
多執行緒場景下使用 ArrayList,要注意這幾點!
ArrayList 不是執行緒安全的,這點很多人都知道,但是執行緒不安全的原因及表現,怎麼在多執行緒情況下使用ArrayList,可能不是很清楚,這裡總結一下。
1. 原始碼分析
檢視 ArrayList 的 add 操作原始碼如下:
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
// 判斷列表的capacity容量是否足夠,是否需要擴容
ensureCapacityInternal(size + 1); // Increments modCount!!
// 將元素新增進列表的元素數組裡面
elementData[size++] = e;
return true;
}
原始碼中涉及的幾個元素及方法定義如下:
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/**
* 列表元素集合陣列
* 如果新建ArrayList物件時沒有指定大小,那麼會將EMPTY_ELEMENTDATA賦值給elementData,
* 並在第一次新增元素時,將列表容量設定為DEFAULT_CAPACITY
*/
transient Object[] elementData;
/**
*列表大小,elementData中儲存的元素個數
*/
private int size;
private void ensureCapacityInternal (int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
通過原始碼可以看出:ArrayList的實現主要就是用了一個Object的陣列,用來儲存所有的元素,以及一個size變數用來儲存當前陣列中已經添加了多少元素。
執行add方法時,主要分為兩步:
- 首先判斷elementData陣列容量是否滿足需求——》判斷如果將當前的新元素加到列表後面,列表的elementData陣列的大小是否滿足,如果size+1的這個需求長度大於了elementData這個陣列的長度,那麼就要對這個陣列進行擴容;
- 之後在elementData對應位置上設定元素的值。
2. 執行緒不安全的兩種體現
2.1 陣列越界異常 ArrayIndexOutOfBoundsException
由於ArrayList新增元素是如上面分兩步進行,可以看出第一個不安全的隱患,在多個執行緒進行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.
2.2 元素值覆蓋和為空問題
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的位置上開始。
3. 程式碼示例
如下,通過兩個執行緒對ArrayList新增元素,可以復現上面的兩種不安全情況。
import java.util.ArrayList;
import java.util.List;
public class ArrayListSafeTest {
public static void main(String[] args) throws InterruptedException {
final List<Integer> list = new ArrayList<Integer>();
// 執行緒A將1-1000新增到列表
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
list.add(i);
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
// 執行緒B將1001-2000新增到列表
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1001; 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));
}
}
}
執行過程中,兩種情況出現如下:
4. ArrayList執行緒安全處理
4.1 Collections.synchronizedList
最常用的方法是通過 Collections 的 synchronizedList 方法將 ArrayList 轉換成執行緒安全的容器後再使用。
List<Object> list =Collections.synchronizedList(new ArrayList<Object>);
4.2 為list.add()方法加鎖
synchronized(list.get()) {
list.get().add(model);
}
4.3 CopyOnWriteArrayList
使用執行緒安全的 CopyOnWriteArrayList 代替執行緒不安全的 ArrayList。
List<Object> list1 = new CopyOnWriteArrayList<Object>();
4.4 使用ThreadLocal
使用ThreadLocal變數確保執行緒封閉性(封閉執行緒往往是比較安全的, 但由於使用ThreadLocal封裝變數,相當於把變數丟進執行執行緒中去,每new一個新的執行緒,變數也會new一次,一定程度上會造成效能[記憶體]損耗,但其執行完畢就銷燬的機制使得ThreadLocal變成比較優化的併發解決方案)。
ThreadLocal<List<Object>> threadList = new ThreadLocal<List<Object>>() {
@Override
protected List<Object> initialValue() {
return new ArrayList<Object>();
}
};