2010-08-28 21 views
22

在什麼情況下,一個不同步的集合(比如ArrayList)會導致問題?我想不出任何人,請問有人可以給我一個ArrayList導致問題和Vector解決它的例子嗎?我寫了一個程序,它有兩個線程同時修改一個有一個元素的數組列表。一個線程將「bbb」放入數組列表中,而另一個線程將「aaa」放入數組列表中。我真的沒有看到一個字符串被修改了一半的實例,我在這裏的正確軌道上?另外,我記得有人告訴我,多個線程並不真正同時運行,一個線程運行一段時間,另一個線程運行後(在具有單個CPU的計算機上)。如果這是正確的,兩個線程如何能夠同時訪問相同的數據?也許線程1將在修改某些內容時停止,線程2將會啓動?Java中的ArrayList和多線程

很多預先感謝。

+5

字符串的一半和一半是不可能的,但如果有兩個線程將100個字符串添加到一個未同步的列表中,那麼最後可能不會獲得200個字符串。 – polygenelubricants 2010-08-28 03:28:59

+0

是的。請參閱我的答案,瞭解爲什麼會發生這種情況。 – 2010-08-28 05:14:05

+0

感謝您的詳細解釋和例子 – user433500 2010-08-28 20:37:24

回答

2

什麼時候會引起麻煩?

任何時候一個線程正在讀取ArrayList,另一個正在寫入,或者它們都在寫入時。 Here's a very known example.

另外,我記得有人告訴我, 多線程是不是真的 同時運行,1線是 了一段時間的運行,而另一個線程 運行後(計算機上有 單個CPU)。如果這是正確的,那麼 兩個線程可以同時訪問相同的 數據?也許線程1 將停止在 中間修改一些東西,線程2將 被啓動?

是的,單核CPU在同一時間只能執行一條指令(不是真的,pipelining已經在這裏一段時間,但作爲一個教授曾經說過,這就是「自由」並行)。儘管在計算機上運行的每個進程只執行一段時間,然後進入空閒狀態。在那一刻,另一個進程可能會開始/繼續執行。然後進入閒置狀態或完成。進程執行是交錯的。

線程發生同樣的事情,只有它們包含在進程內。它們的執行方式取決於操作系統,但概念保持不變。他們在一生中不斷從活躍變爲閒暇。

4

我真的沒有看到一個字符串被修改了一半的實例,我在這裏的正確軌道上?

這不會發生。但是,可能發生的情況是隻有一個字符串被添加。或者在添加呼叫期間發生異常。

有人請給我一個例子,其中一個ArrayList導致一個問題,一個Vector解決它嗎?

如果要從多個線程訪問集合,則需要同步此訪問權限。但是,使用Vector並不能真正解決問題。你不會得到上述的問題,但下面的模式將還是不行:

// broken, even though vector is "thread-safe" 
if (vector.isEmpty()) 
    vector.add(1); 

載體本身不會遭到損壞,但是,這並不意味着它不能進入​​狀態,你的業務邏輯不會想要有。 您需要在您的應用程序代碼中同步(然後不需要使用Vector)。

synchronized(list){ 
    if (list.isEmpty()) 
    list.add(1); 
} 

併發實用程序包還有一些集合,它們提供線程安全隊列等所需的原子操作。

21

一個實際的例子。在最後的列表中應該包含40個項目,但對我來說,它通常顯示30到35之間。猜猜爲什麼?

static class ListTester implements Runnable { 
    private List<Integer> a; 

    public ListTester(List<Integer> a) { 
     this.a = a; 
    } 

    public void run() { 
     try { 
      for (int i = 0; i < 20; ++i) { 
       a.add(i); 
       Thread.sleep(10); 
      } 
     } catch (InterruptedException e) { 
     } 
    } 
} 


public static void main(String[] args) throws Exception { 
    ArrayList<Integer> a = new ArrayList<Integer>(); 

    Thread t1 = new Thread(new ListTester(a)); 
    Thread t2 = new Thread(new ListTester(a)); 

    t1.start(); 
    t2.start(); 
    t1.join(); 
    t2.join(); 
    System.out.println(a.size()); 
    for (int i = 0; i < a.size(); ++i) { 
     System.out.println(i + " " + a.get(i)); 
    } 
} 

編輯
有很多種全面的解釋周圍(例如,斯蒂芬ç的帖子),但我會做一點評論,因爲mfukar問。 (應該馬上做,當發佈答案時)

這是從兩個不同的線程遞增整數的着名問題。 Sun的Java教程中有關於併發性的nice explanation。只有在這個例子中,他們有--i++i,我們有++size兩次。 (++sizeArrayList#add實現的一部分。)

+0

+1個不錯的例子。比賽條件錯誤通常無法重現,而且這樣的例子對新手來說很有幫助。 – 2010-08-28 04:45:39

+0

爲什麼?一個公平的例子,但你的答案是不完整的。 – 2010-08-28 05:48:34

+0

@mfukar謝謝,我剛剛發佈它,並去睡覺:)現在鏈接添加。 – 2010-08-28 15:47:56

1

youe查詢的第一部分已被回答。我會盡量回答第二部分:

另外,我記得有人告訴我,多線程是不是真的同時運行,1個線程在運行了一段時間,並紛紛跟帖運行(計算機上有一箇中央處理器)。如果這是正確的,兩個線程如何能夠同時訪問相同的數據?也許線程1將在修改某些內容時停止,線程2將會啓動?

在wait-notify框架中,獲取對象上的鎖的線程在等待某種條件時釋放它。生產者 - 消費者問題就是一個很好的例子。請看這裏:link text

28

如果在沒有足夠的同步的情況下使用ArrayList(例如),可能會出錯的有三個方面。

第一種情況是如果兩個線程同時發生更新ArrayList,那麼它可能會損壞。例如,附加到列表的邏輯是這樣的:

public void add(T element) { 
    if (!haveSpace(size + 1)) { 
     expand(size + 1); 
    } 
    elements[size] = element; 
    // HERE 
    size++; 
} 

現在假設我們有一個處理器/核心和兩個線程「同時」執行相同的列表上的該代碼。假設第一個線程到達標記爲HERE的點並被搶佔。第二個線程出現,並覆蓋elements中的第一個線程剛剛使用自己的元素更新的插槽,然後遞增size。當第一個線程終於得到控制時,它將更新size。最終的結果是我們添加了第二個線程的元素,而不是第一個線程的元素,並且很可能還會將null添加到列表中。(這只是示意性的。在現實中,本地代碼編譯器可能重新排序的代碼,等等。但問題是,如果更新同時發生不好的事情都可能發生。)

第二個場景的出現是由於主存儲器內容緩存在CPU的高速緩衝存儲器中。假設我們有兩個線程,一個添加元素到列表中,另一個讀取列表的大小。當線程添加一個元素時,它將更新列表的size屬性。但是,由於size不是volatile,因此size的新值可能不會立即寫入主存儲器。相反,它可以位於緩存中,直到Java內存模型要求刷新緩存寫入的同步點。同時,第二個線程可以在列表上調用size()並獲得陳舊值size。在最壞的情況下,第二個線程(例如調用get(int))可能會看到sizeelements數組的值不一致,從而導致意外的異常。 (請注意,即使只有一個內核並且沒有內存緩存,也可能會發生這種問題。JIT編譯器可以自由使用CPU寄存器來緩存內存內容,並且這些寄存器不會因其內存位置而被刷新/刷新當發生線程上下文切換時)

第三種情況出現在您同步ArrayList時的操作;例如通過將其包裝爲SynchronizedList

List list = Collections.synchronizedList(new ArrayList()); 

    // Thread 1 
    List list2 = ... 
    for (Object element : list2) { 
     list.add(element); 
    } 

    // Thread 2 
    List list3 = ... 
    for (Object element : list) { 
     list3.add(element); 
    } 

如果線程2的列表是一個ArrayListLinkedList和兩個線程同時運行,線程2將失敗,ConcurrentModificationException。如果是其他(家庭釀造)列表,則結果是不可預測的。問題在於製作list同步列表不夠充分,以使它對不同線程執行的列表操作的序列是線程安全的。爲了實現這一點,應用程序通常需要在更高級別/更粗粒度的同步。


另外,我記得有人告訴我,多線程並沒有真正運行的同時,1個線程在運行了一段時間,並紛紛跟帖運行(計算機上有一個CPU)。

正確。如果只有一個內核可用來運行應用程序,顯然一次只能運行一個線程。這使得某些危害變得不可能,而其他危險發生的可能性也就越小。但是,操作系統可能會隨時在代碼中的任意位置從一個線程切換到另一個線程。

如果這是正確的,兩個線程怎麼能同時訪問相同的數據?也許線程1將在修改某些內容時停止,線程2將會啓動?

是。這是可能的。它發生的概率非常小,但這只是使這類問題更加陰險。


1 - 這是因爲線程時間分片的事件是極其罕見的,當上的硬件時鐘週期時間刻度測量。

+0

Hi Stephen。我知道將synchronized關鍵字添加到線程同時進入的函數可以解決問題嗎?我的意思是,在你的第一個例子中,簡單地將函數定義爲「public synchronized void add(T element)」解決了問題,不是嗎? – CAAY 2016-02-15 11:56:22

+0

@CAAY - 假設是。但是,該代碼是真正的「ArrayList.add」方法中發生的事情的「草圖」。顯然,你不能將'synchronized'加入到由標準類定義的方法中。所以這不是這個問題描述的問題的解決方案。 – 2016-05-26 07:36:08

0

您無法控制何時一個線程將被停止,並且其他線程將啓動。線程1不會等到它完全完成添加數據。總有可能破壞數據。