JUC(4):集合類不安全
List不安全
在之前我們寫list相關的程式碼時,大部分都是用的單執行緒,所以不存線上程安全問題!
public class ListTest {
public static void main(String[] args) {
//併發下 ArrayList 不安全
List<String> list = new ArrayList<>();
for (int i =1 ;i <=100 ;i++){
new Thread(()->{
list. add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
執行之後會報錯:java.util.ConcurrentModificationException 併發修改異常。
解決的方法:
1.用Vector代替ArrayList;不建議使用!檢視底層可以發現,ArrayList的add方法沒有被Synchronized修飾,而Vector的是被Synchronized修飾的!
List<String> list = Collections.synchronizedList(new ArrayList<>());
上述兩種方法是常人最容易想到的!
3.使用CopyOnWriteArrayList:其中CopyOnWrite 寫入時複製,又叫COW 是計算機程式設計領域的一種優化策略!(推薦使用)
List<String> list = new CopyOnWriteArrayList<>();
問題:為什麼使用CopyOnWriteArrayList而不使用Vector?
Vector和CopyOnWriteArrayList都是執行緒安全的List,底層都是陣列實現的,Vector的每個方法都進行了加鎖,而CopyOnWriteArrayList的讀操作是不加鎖的,因此CopyOnWriteArrayList的讀效能遠高於Vector,Vector每次擴容的大小都是原來陣列大小的2倍,而CopyOnWriteArrayList不需要擴容,通過COW思想就能使陣列容量滿足要求。兩個集合都實現了RandomAccess介面,支援隨機讀取,因此更加推薦使用for迴圈進行遍歷。在開發中,讀操作會遠遠多於其他操作,因此使用CopyOnWriteArrayList集合效率更高。
什麼是寫入時複製(COW)的思想?
寫入時複製(CopyOnWrite,簡稱COW)思想是計算機程式設計領域中的一種通用優化策略。其核心思想是,如果有多個呼叫者(Callers)同時訪問相同的資源(如記憶體或者是磁碟上的資料儲存),他們會共同獲取相同的指標指向相同的資源,直到某個呼叫者修改資源內容時,系統才會真正複製一份專用副本(private copy)給該呼叫者,而其他呼叫者所見到的最初的資源仍然保持不變。這過程對其他的呼叫者都是透明的(transparently)。此做法主要的優點是如果呼叫者沒有修改資源,就不會有副本(private copy)被建立,因此多個呼叫者只是讀取操作時可以共享同一份資源。
通俗易懂的講,寫入時複製技術就是不同程序在訪問同一資源的時候,只有更新操作,才會去複製一份新的資料並更新替換,否則都是訪問同一個資源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是採用了 COW 思想,它是如何工作的呢?簡單來說,就是平時查詢的時候,都不需要加鎖,隨便訪問,只有在更新的時候,才會從原來的資料複製一個副本出來,然後修改這個副本,最後把原資料替換成當前的副本。修改操作的同時,讀操作不會被阻塞,而是繼續讀取舊的資料。這點要跟讀寫鎖區分一下。
Set不安全
同樣的,使用Set多執行緒也是不安全的。
public class SetTest {
public static void main(String[] args) {
Set<Object> set = new HashSet<>();
for (int i =1 ;i <=100 ;i++){
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
也會發生java.util.ConcurrentModificationException 併發修改異常。
解決方法:
1.使用Collections工具類,將上面的程式碼改為:
Set<String> set = Collections.synchronizedList(new HashSet<>());
2.使用CopyOnWriteArraySet:
Set<Object> set = new CopyOnWriteArraySet<>();
問題:HashSet的底層是什麼?
其中Set的add方法底層:
HsahSet底層使用的是HashMap;可以看到add方法的本質就還是map,其中的e就是map中的key,PRESENT就是一個不變的值。
Map 不安全
public class MapTest {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i <=30 ;i++){
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
也會發生java.util.ConcurrentModificationException 併發修改異常。
解決方法
1.使用ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();
注意的是:ConcurrentHashMap不能接受null的key和null的value,會丟擲空指標異常的!HashMap不能保證元素的順序,HashMap能夠將鍵設為null,也可以將值設為null。