為什么ArrayList線程不安全?

一.概述

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

面試時相信面試官首先就會問到關(guān)于它的知識。一個經(jīng)常被問到的問題就是:ArrayList是否是線程安全的?

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

二.源碼分析

首先看看這個類所擁有的部分屬性字段:

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

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

}

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

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

public boolean add(E e) {

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

}

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

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

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

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

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

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

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

三.案例復現(xiàn)

我們用如下的代碼可以進行安全性的校驗:

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

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

}
————————————————
版權(quán)聲明:本文為CSDN博主「Zorrooooo」的原創(chuàng)文章,遵循CC 4.0 by-sa版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/u012859681/article/details/78206494

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容