12

Java有線程安全版本HashMap,命名爲ConcurrentHashMap,線程安全版本TreeMap命名爲ConcurrentSkipListMap,但HashSet沒有ConcurrentHashSet什麼時候CopyOnWriteArraySet對實現線程安全的HashSet有用?

相反,通常有4種方式使用線程安全Set

  1. Set<String> mySet = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
  2. Set<String> s = Collections.synchronizedSet(new HashSet<String>());
  3. ConcurrentSkipListSet<E>
  4. CopyOnWriteArraySet<E>

1使用ConcurrentHashMapkeySet()實現均爲Set和線程安全。

2使用​​的方式,似乎不推薦這種方式。

3是基於ConcurrentSkipListMap而被廣泛使用。

4基於CopyOnWriteArrayList,因此它具有相同的基本屬性CopyOnWriteArrayList。以下是選擇從CopyOnWriteArraySet DOC:http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CopyOnWriteArraySet.html

  • 它是最適合於中集大小一般留 小,只讀操作遠多於可變操作應用程序,並 你需要防止遍歷期間線程間的干擾。
  • 它是線程安全的。
  • 突變操作(添加,設置,刪除等)很昂貴,因爲它們通常需要複製整個底層陣列。
  • 迭代器不支持可變刪除操作。
  • 通過迭代器遍歷很快,不會受到來自其他線程的干擾。
  • 迭代器構建時,迭代器依賴於數組的不變快照。

由於1和3是常用的,爲什麼CopyOnWriteArraySet存在? CopyOnWriteArraySet何時有用?

補充:CopyOnWriteArraySet基於CopyOnWriteArrayList,並在List數據結構contains操作是O(n),而Set數據結構是高性能contains操作,可能有人解釋一下嗎?

+0

在JDK中確實沒有這樣的本地類;你可以使用'Collections.newSetFromMap(new ConcurrentHashMap <>())'。 – fge 2015-03-25 07:25:24

+1

另一個有用的材料:http://stackoverflow.com/questions/6720396/different-types-of-thread-safe-sets-in-java – coderz 2015-03-27 16:53:02

回答

12

當您有一個線程安全集合的一小組元素時,它非常有用。

一個例子是一組聽衆。您需要確保唯一性並有效地對它們進行迭代。

BTW CopyOnWriteArraySet具有每個參考基礎上的最低開銷。它可能只有其他館藏的1/6。如果你有很多它們,這是特別有用的。

while set data structure for high performance contains operation,有誰能解釋這個嗎?

COWAS在內存方面效率更高,contains比其他方案更快。什麼是「高性能」取決於用例。

+2

謝謝您的回覆!你能解釋爲什麼''CopyOnWriteArraySet'是如此節省空間? – coderz 2015-03-25 07:47:06

+1

它可能比'HashSet'小,但我無法想象爲什麼它會比說'ArrayList'更小,特別是因爲他們說他們做線程本地快照。我敢打賭,它甚至無法擊敗'TreeMap' /'TreeSet'。 – VoidStar 2015-03-25 08:07:23

+1

@coderz的CopyOnWriteArraySet包裝引用數組,這意味着它可以每基準(即使在64位JVM)但是其它組都建立在地圖這反過來每個元件具有Map.Entry的使用盡可能少爲4字節。 Map.Entry約爲24個字節加上對該條目的引用,使得每個元素最多爲32個字節,具體取決於集合。 – 2015-03-25 08:19:21

4

寫入時複製結構在功能不變。

的Java在一個點有一個非常可憐的故事,以提供諸如集上寫的結構不變的看法。例如,如果你有一個成員,並且你公開地返回它,調用者可以轉過來編輯它,因此編輯你的對象的內部狀態!但你還能做什麼,在從任何公共職能返回之前複製整個事情?這將是毫無意義的緩慢。

這是Java歷史上較早的故事。他們幾乎完全依賴於不可變的對象(字符串是一個例子)。集合是這種模式的一個例外,因此從封裝的角度來看是有問題的。當加入CopyOnWriteArraySetunmodifiableCollectionunmodifiableSet還不存在(雖然unmodifiableCollection在很大程度上解決了這個問題,我仍然覺得它比其他語言提供了一個更麻煩的解決方案,使用自定義的數據結構,尤其是當)。所以這可能解釋了首先創建CopyOnWriteArraySet的最大動機。您可以返回CopyOnWriteArraySet,而不用擔心別人會修改對象的內部狀態,也不會浪費時間製作不必要的副本。

寫入時複製是一種時尚,幾年前,但它是多線程編程出了名的低效想法,比其他車型低效率。從你發佈的文檔中,他們通過創建線程本地快照來加快迭代,這意味着他們花費內存來彌補。所以只要你的數據很小就可以使用它,因爲內存快照不會浪費太多內存。

+0

對於正確的用例CopyOnWriteArraySet是最有效的。使用不正確,任何集合都可能被認爲效率低下。 – 2015-03-25 08:23:14

+0

'CopyOnWriteArraySet'對於大多數開發者來說是一個簡單和高效的完美合理的平衡,但COW並不是「最高效」的方式。請記住,即使在同一個線程中,每次創建新的迭代器時,它都會構造一個新的快照。通常多線程環境中的COW在特殊情況下只是最優的,並且不能像效率一樣讓開發人員決定何時需要副本。具有手動鎖定組合的'ArrayList'避免了不必要的拷貝,但是當它們幫助時仍然支持拷貝。 – VoidStar 2015-03-25 09:32:46

+0

新的Iterator不創建快照,它使用不可變引用數組。這就是爲什麼每當你改變數組的內容時它都必須進行寫入操作。 – 2015-03-25 09:36:59

0

測試代碼:

Set<String> a = new CopyOnWriteArraySet<String>(); 
    for(int i=0;i<10;i++) { 
     a.add("str" + i); 
    } 
    boolean flag = true; 
    long t1 = System.currentTimeMillis(); 
    for(int i=0;i<200000;i++) { 
     flag = a.contains("str" + i); 
    } 
    System.out.println(System.currentTimeMillis() - t1); 

    Set<String> b = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>()); 
    for(int i=0;i<10;i++) { 
     b.add("str" + i); 
    } 
    t1 = System.currentTimeMillis(); 
    for(int i=0;i<200000;i++) { 
     flag = b.contains("str" + i); 
    } 
    System.out.println(System.currentTimeMillis() - t1); 

可見CopyOnWriteArraySetCollections.newSetFromMap慢。由於測試用例是一個非常小的Set,只讀操作,CopyOnWriteArraySet似乎並不更好。

+1

'CopyOnWriteArraySet'的用例與用於'Collections.newSetFromMap'的用例不同。您的評估完全有缺陷。 – 2015-03-27 16:56:49

+0

@JohnVint你能講更多的細節嗎? – 2015-03-28 02:17:56

+0

當然。 CopyOnWriteArrayList非常好,當你做90 +%的讀取。如果你寫很多,這是非常昂貴和低效率。當你想用多線程安全地迭代列表而不必同步整個集合時,COWAL也是非常好的。 Collections.synchronizedList將會有更快的添加,所以對於簡單的放入和移除可能會有好處,但是其他的不會。 – 2015-03-29 23:11:25