容器之List接口下各實現類(Vector,ArrayList 和LinkedList)的線程安全問題


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元素時,實際做了兩個大的步驟:

  1. 判斷elementData數組容量是否滿足需求
  2. 在elementData對應位置上設置值

這樣也就出現了第一個導致線程不安全的隱患,在多個線程進行add操作時可能會導致elementData數組越界。具體邏輯如下:

  1. 列表大小為9,即size=9
  2. 線程A開始進入add方法,這時它獲取到size的值為9,調用ensureCapacityInternal方法進行容量判斷。
  3. 線程B此時也進入add方法,它獲取到size的值也為9,也開始調用ensureCapacityInternal方法。
  4. 線程A發現需求大小為10,而elementData的大小就為10,可以容納。於是它不再擴容,返回。
  5. 線程B也發現需求大小為10,也可以容納,返回。
  6. 線程A開始進行設置值操作, elementData[size++] = e 操作。此時size變為10。
  7. 線程B也開始進行設置值操作,它嘗試設置elementData[10] = e,而elementData沒有進行過擴容,它的下標最大為9。於是此時會報出一個數組越界的異常ArrayIndexOutOfBoundsException.

另外再看第二步 elementData[size++] = e 設置值的操作同樣會導致線程不安全。從這兒可以看出,這步操作也不是一個原子操作,它由如下兩步操作構成:

  1. elementData[size] = e;
  2. size = size + 1;

在單線程執行這兩條代碼時沒有任何問題,但是當多線程環境下執行時,可能就會發生一個線程的值覆蓋另一個線程添加的值,具體邏輯如下:

  1. 列表大小為0,即size=0;
  2. 線程A開始添加一個元素,值為A。此時它執行第一條操作,將A放在了elementData下標為0的位置上;
  3. 接着線程B剛好也要開始添加一個值為B的元素,且走到了第一步操作。此時線程B獲取到size的值依然為0,於是它將B也放在了elementData下標為0的位置上;
  4. 線程A開始將size的值增加為1;
  5. 線程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...

 

參考:

1. arraylist為何是線程不安全的?

2. Vector到底是不是線程安全的?

3.LinkedList為何是線程不安全的?

4.LinkedList線程不安全該如何解決?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM