一、ArrayList與LinkedList的比較?
ArrayList從原理上就是數據結構中的數組,也就是內存中一片連續的空間,這意味着,當我get(index)的時候,我可以根據數組的(首地址+偏移量),直接計算出我想訪問的第index個元素在內存中的位置。
LinkedList可以簡單理解為數據結構中的鏈表(說簡單理解,因為其實是雙向循環鏈表),在內存中開辟的不是一段連續的空間,而是每個元素有一個[元素|下一元素地址]這樣的內存結構。當get(index)時,只能從首元素開始,依次獲得下一個元素的地址。
用時間復雜度表示的話,ArrayList的get(n)是o(1),而LinkedList是o(n)。
二、ArrayList為什么不是線程安全的?
ArrayList的實現主要就是用了一個Object的數組elementData,用來保存所有的元素,以及一個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數組越界。具體邏輯如下:
- 列表大小為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.
假設當線程A和線程B都執行add操作時,它們獲得的size相等,比如都為9,而數組的容量為10,線程AB都通過了判斷不需要擴容。然后當執行elementData[size++]=e操作時,線程A的執行為elementData[9]=e,然后size++=10;
然后線程B執行時 執行語句為elementData[10]=e,size++;而數組並沒有擴容【0~9】,這時會發生數組越界。
另外第二步 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的位置上開始。
https://blog.csdn.net/u012859681/article/details/78206494
三、線程安全的List
ArrayList 不是線程安全的,如果遇到多線程場景,可以通過 Collections 的 synchronizedList 方法將其轉換成線程安全的容器后再使用。例如像下面這樣:
List<String> synchronizedList = Collections.synchronizedList(list); synchronizedList.add("aaa"); synchronizedList.add("bbb"); for (int i = 0; i < synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); }
