Java中實現執行緒安全HashSet的幾種方法 | baeldung

banq發表於2022-01-27

在本教程中,我們將瞭解建立執行緒安全HashSet例項的可能性以及HashSet的ConcurrentHashMap的等價物。此外,我們將研究每種方法的優缺點。

 

使用ConcurrentHashMap工廠方法的執行緒安全HashSet

首先,我們將檢視公開靜態newKeySet()方法的ConcurrentHashMap類。基本上,此方法返回一個尊重java.util.Set介面的例項,並允許使用標準方法,如add()、contains()等。

這可以簡單地建立為:

Set<Integer> threadSafeUniqueNumbers = ConcurrentHashMap.newKeySet();
threadSafeUniqueNumbers.add(23);
threadSafeUniqueNumbers.add(45);

此外,返回的Set的效能類似於Has hSet,因為兩者都是使用基於雜湊的演算法實現的。此外,同步邏輯帶來的額外開銷也很小,因為實現使用了ConcurrentHashMap。

最後,缺點是該方法僅從 Java 8 開始存在。

 

使用ConcurrentHashMap例項方法的執行緒安全HashSet

至此,我們已經瞭解了ConcurrentHashMap 的靜態方法。接下來,我們將處理可用於ConcurrentHashMap的例項方法來建立執行緒安全的Set例項。有兩種方法可用,newKeySet()和newKeySet(defaultValue),它們彼此略有不同。

這兩種方法都建立了一個與原始map連結的Set。換句話說,每次我們向原始ConcurrentHashMap 新增一個新條目時, Set都會接收該值。此外,讓我們看看這兩種方法之間的區別。

  • newKeySet ()方法

如上所述,newKeySet()公開了一個包含原始對映的所有鍵的Set 。此方法與newKeySet(defaultValue)的主要區別在於當前方法不支援向Set新增新元素。因此,如果我們嘗試呼叫add()或addAll() 之類的方法,我們將得到 UnsupportedOperationException。

儘管remove(object)或clear()之類的操作按預期工作,但我們需要注意Set上的任何更改都將反映在原始對映map中:

ConcurrentHashMap<Integer,String> numbersMap = new ConcurrentHashMap<>();
Set<Integer> numbersSet = numbersMap.keySet();

numbersMap.put(1, "One");
numbersMap.put(2, "Two");
numbersMap.put(3, "Three");

System.out.println("Map before remove: "+ numbersMap);
System.out.println("Set before remove: "+ numbersSet);

numbersSet.remove(2);

System.out.println("Set after remove: "+ numbersSet);
System.out.println("Map after remove: "+ numbersMap);

輸出:

Map before remove: {1=One, 2=Two, 3=Three}
Set before remove: [1, 2, 3]

Set after remove: [1, 3]
Map after remove: {1=One, 3=Three}

 

  • newKeySet (defaultValue)方法

讓我們看看另一種使用地圖中的鍵建立Set的方法。與上面提到的相比,newKeySet(defaultValue)返回一個Set例項,該例項支援通過呼叫set 上的add()或addAll()來新增新元素。

進一步檢視作為引數傳遞的預設值,這被用作地圖中新增的每個新條目的值add()或addAll()方法。以下示例顯示了它是如何工作的:

ConcurrentHashMap<Integer,String> numbersMap = new ConcurrentHashMap<>();
Set<Integer> numbersSet = numbersMap.keySet("SET-ENTRY");

numbersMap.put(1, "One");
numbersMap.put(2, "Two");
numbersMap.put(3, "Three");

System.out.println("Map before add: "+ numbersMap);
System.out.println("Set before add: "+ numbersSet);

numbersSet.addAll(asList(4,5));

System.out.println("Map after add: "+ numbersMap);
System.out.println("Set after add: "+ numbersSet);

下面是上面程式碼的輸出:

Map before add: {1=One, 2=Two, 3=Three}
Set before add: [1, 2, 3]
Map after add: {1=One, 2=Two, 3=Three, 4=SET-ENTRY, 5=SET-ENTRY}
Set after add: [1, 2, 3, 4, 5]

 

使用集合實用程式類的執行緒安全HashSet

讓我們使用java.util.Collections 中可用的synchronizedSet()方法來建立一個執行緒安全的HashSet例項:

Set<Integer> syncNumbers = Collections.synchronizedSet(new HashSet<>());
syncNumbers.add(1);

在使用這種方法之前,我們需要意識到它的效率不如上面討論的那些。與實現低階併發機制的ConcurrentHashMap相比, synchronizedSet()基本上 只是將Set例項包裝到同步裝飾器中。

 

使用CopyOnWriteArraySet 的執行緒安全集

建立執行緒安全Set實現的最後一種方法是CopyOnWriteArraySet。建立這個Set的例項很簡單:

Set<Integer> copyOnArraySet = new CopyOnWriteArraySet<>();
copyOnArraySet.add(1);

儘管使用這個類看起來很有吸引力,但我們需要考慮一些嚴重的效能缺陷。在幕後,CopyOnWriteArraySet使用Array 而不是HashMap來儲存資料。這意味著像contains()或remove()這樣的操作有 O(n) 的複雜度,而當使用由ConcurrentHashMap 支援的 Set 時,複雜度是 O(1)。

建議在Set大小通常保持較小且只讀操作佔多數時使用此實現。

 

結論

在本文中,我們看到了建立執行緒安全Set例項的不同可能性,並強調了它們之間的區別。首先我們看到了ConcurrentHashMap.newKeySet() 靜態方法。當需要執行緒安全的HashSet時,這應該是首選。之後我們檢視了ConcurrentHashMap靜態方法和  用於ConcurrentHashMap 例項的newKeySet()、newKeySet(defaultValue)之間的區別。

最後我們還討論了集合。synchronizedSet()和CopyOnWriteArraySet 存在效能缺陷。

像往常一樣,完整的原始碼可以在 GitHub 上找到

 

相關文章