Vector 、ArrayList 和LinkedList都是List接口下的實現類,但是他們之間的區別和聯系是什么呢?
首先:
然后:
如果您僅僅想知道結論,那么可以關閉了。
下面我討論討論為什么。
- 發現arrayList的線程安全是由size引起的,為何這么說呢?
這是ArrayList所擁有的部分屬性,通過這兩個字段我們可以看出,ArrayList的實現主要就是用了一個Object的數組,用來保存所有的元素,以及一個size變量用來保存當前數組中已經添加了多少元素。
1 public class ArrayList<E> extends AbstractList<E> 2 implements List<E>, RandomAccess, Cloneable, java.io.Serializable 3 { 4 /** 5 * 列表元素集合數組 6 * 如果新建ArrayList對象時沒有指定大小,那么會將EMPTY_ELEMENTDATA賦值給elementData, 7 * 並在第一次添加元素時,將列表容量設置為DEFAULT_CAPACITY 8 */ 9 transient Object[] elementData; 10 11 /** 12 * 列表大小,elementData中存儲的元素個數 13 */ 14 private int size; 15 }
接着我們看下最重要的add操作時的源代碼:
1 public boolean add(E e) { 2 3 /** 4 * 添加一個元素時,做了如下兩步操作 5 * 1.判斷列表的capacity容量是否足夠,是否需要擴容 6 * 2.真正將元素放在列表的元素數組里面 7 */ 8 ensureCapacityInternal(size + 1); // Increments modCount!! 9 elementData[size++] = e; 10 return true; 11 }
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的位置上開始。
舉例分析:
1 public static void main(String[] args) throws InterruptedException { 2 final List<Integer> list = new ArrayList<Integer>(); 3 4 // 線程A將0-1000添加到list 5 new Thread(new Runnable() { 6 public void run() { 7 for (int i = 0; i < 1000 ; i++) { 8 list.add(i); 9 10 try { 11 Thread.sleep(1); 12 } catch (InterruptedException e) { 13 e.printStackTrace(); 14 } 15 } 16 } 17 }).start(); 18 19 // 線程B將1000-2000添加到列表 20 new Thread(new Runnable() { 21 public void run() { 22 for (int i = 1000; i < 2000 ; i++) { 23 list.add(i); 24 25 try { 26 Thread.sleep(1); 27 } catch (InterruptedException e) { 28 e.printStackTrace(); 29 } 30 } 31 } 32 }).start(); 33 34 Thread.sleep(1000); 35 36 // 打印所有結果 37 for (int i = 0; i < list.size(); i++) { 38 System.out.println("第" + (i + 1) + "個元素為:" + list.get(i)); 39 } 40 }
結果:
第7個元素為:3 第8個元素為:1003 第9個元素為:4 第10個元素為:1004 第11個元素為:null 第12個元素為:1005 第13個元素為:6
可以看到第11個元素的值為null,這也就是我們上面所說的情況。
- Vector是線程安全的,為什么?
vector之所以說是線程安全的是因為,他的很多方法上都加了Synchronized關鍵字。
但是!但是!但是!這僅僅表示在vector內部,其所有方法不會被多線程所訪問。
如果是像下面這樣呢?
if (!vector.contains(element)) vector.add(element); ... }
這是經典的 put-if-absent 情況,盡管 contains, add 方法都正確地同步了,但作為 vector 之外的使用環境,仍然存在 race condition: 因為雖然條件判斷 if (!vector.contains(element))與方法調用 vector.add(element); 都是原子性的操作 (atomic),但在 if 條件判斷為真后,那個用來訪問vector.contains 方法的鎖已經釋放,在即將的 vector.add 方法調用之間有間隙,在多線程環境中,完全有可能被其他線程獲得 vector的 lock 並改變其狀態, 此時當前線程的vector.add(element); 正在等待(只不過我們不知道而已)。只有當其他線程釋放了 vector 的 lock 后,vector.add(element); 繼續,但此時它已經基於一個錯誤的假設了。
所以,單個的方法 synchronized 了並不代表組合(compound)的方法調用具有原子性,使 compound actions 成為線程安全的可能解決辦法之一還是離不開intrinsic lock (這個鎖應該是 vector 的,但由 client 維護):
// Vector v = ... public boolean putIfAbsent(E x) { synchronized(v) { boolean absent = !contains(x); if (absent) { add(x); } } return absent; }
【綜上】:
Vector 和 ArrayList 實現了同一接口 List, 但所有的 Vector 的方法都具有 synchronized 關鍵修飾。但對於復合操作,Vector 仍然需要進行同步處理。
- 關於LinkedList線程不安全,為什么?
在多線程程序中有多個線程訪問LinkedList的話會出現什么問題呢?
會拋出ConcurrentModificationException,JDK代碼里,ListItr的add(), next(), previous(), remove(), set()方法都會跑出ConcurrentModificationException。
舉例:通過LinkedList對象修改其結構
如果兩個線程都是通過LinkedList對象修改其結構,會發生什么呢?我們先看一下JDK中LinkedList的數據結構。
這是一個雙向循環鏈表。header的作用就是能快速定位鏈表的頭和尾。圖中,“n”表示next,“p”表示previous。header的n指向first element;p指向last element。
當一個線程A調用LinkedList的addFirst方法時(假設添加節點“4”):
第一步:它首先更新“4”的n和p,n->3, p->header;
第二步:更新節點“3”和herder的n和p,3的n不變, 3的p->4, header的n->4, header的p不變。
假設兩個線程A,B同時調用addFirst(4), addFirst(5),會發生什么呢?
很可能4,5的n指向3,p都指向header。也可能addFirst后,緊接着發現getFirst已經不是剛剛加入的元素。
【解決辦法】
方法一:List<String> list = Collections.synchronizedList(new LinkedList<String>());
方法二:將LinkedList全部換成ConcurrentLinkedQueue;
Over...
參考: