高併發程式設計:初識併發容器類
JDK5.0以後提供了多種併發類容器來替代同步容器類從而改善效能。同步類容器狀態都是序列化的。他們雖然實現了執行緒安全,但是嚴重降低了併發性,在多執行緒環境時,嚴重降低了應用程式的吞吐量。
ConcurrentMap介面
ConcurrentMap介面有兩個重要的實現類:ConcurentHashMap、ConcurrentSkipListMap(支援併發排序功能)。ConcurrentHashMap內部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的HashTable,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。把一個整體分成16個段(Segment),也就是最高支援16個執行緒的併發修改操作。這是在多執行緒場景時減小鎖粒度從而降低鎖競爭的一種方案。
同樣的,為了對比HashMap和ConcurrentMap,我們照上一節的例子來寫測試用例:
public class UseHashMap { public static void main(String[] args) { Map<String, Object> map = new HashMap<String, Object>(); map.put("1", "value1"); map.put("2", "value2"); map.put("3", "value3"); map.put("4", "value4"); map.put("5", "value5"); map.put("6", "value6"); map.put("7", "value7"); map.put("8", "value8"); map.put("9", "value9"); map.put("10", "value10"); System.out.println(map); Iterator<String> it = map.keySet().iterator(); while(it.hasNext()) { String key = it.next(); if("3".equals(key)) { // map.put(key, "Hello Me."); map.put("three", "three"); } } System.out.println(map); } }
上述程式碼的在HashMap中放入了10個key和與之對應的value。迴圈迭代時當key等於3的時候,就想map中放入鍵值對three/three。不出意外,該程式碼同樣會丟擲:
Exception in thread "main" java.util.ConcurrentModificationException
換成並非容器類ConcurrentHashMap之後的解決了執行緒安全的問題。
public class UseConcurrentHashMap { public static void main(String[] args) { Map<String, Object> map = new ConcurrentHashMap<String, Object>(); map.put("1", "value1"); map.put("2", "value2"); map.put("3", "value3"); map.put("4", "value4"); map.put("5", "value5"); map.put("6", "value6"); map.put("7", "value7"); map.put("8", "value8"); map.put("9", "value9"); map.put("10", "value10"); System.out.println(map); Iterator<String> it = map.keySet().iterator(); while(it.hasNext()) { String key = it.next(); if("3".equals(key)) { map.put(key+"new", "Hello Me."); } } System.out.println(map); } }
控制檯輸出:
{1=value1, 2=value2, 3=value3, 4=value4, 5=value5, 6=value6, 7=value7, 8=value8, 9=value9, 10=value10}
{1=value1, 2=value2, 3=value3, 4=value4, 5=value5, 3new=Hello Me., 6=value6, 7=value7, 8=value8, 9=value9, 10=value10}
我們發現,已經成功的將key/value放進了map中。
CopyOnWrite類
CopyOnWrite從字面的意思來理解便是寫時複製,意思是當有執行緒對容器內容進行寫的操作的時候,並不是直接在該容器裡進行寫操作,而是先將容器複製一份,再在這份複製出來的容器裡面進行資料的修改,修改結束之後再將原容器的引用指向這個修改過後的新容器,這是一種典型的讀寫分離思想。
借用其他博主的示意圖如下:
當有新元素加入的時候,建立新陣列,並往新陣列中加入一個新元素,這個時候,array這個引用仍然是指向原陣列的。
當元素在新陣列新增成功後,將array這個引用指向新陣列。
CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。
這樣做是為了避免在多執行緒併發add的時候,複製出多個副本出來,把資料搞亂了,導致最終的陣列資料不是我們期望的。我們以CopyOnWrite具體實現類CopyOnWriteArrayList為例子來看看
public class UseCopyOnWriteArrayList {
public static void main(String[] args) throws InterruptedException {
List<String> a = new ArrayList<String>();
a.add("a");
a.add("b");
a.add("c");
final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(a);
Thread t = new Thread(new Runnable() {
int count = 1;
@Override
public void run() {
while(true) {
list.add(count++ + "");
}
}
});
t.setDaemon(true);
t.start();
Thread.currentThread().sleep(3);
for(String s : list) {
System.out.println(list.hashCode());
System.out.println(s);
}
}
}
同樣的這裡給出非併發容器ArrayList來作為對比。
public class UseArrayList {
public static void main(String[] args) throws InterruptedException {
List<String> a = new ArrayList<String>();
a.add("a");
a.add("b");
a.add("c");
final ArrayList<String> list = new ArrayList<String>(a);
Thread t = new Thread(new Runnable() {
int count = -1;
@Override
public void run() {
while (true) {
list.add(count++ + "");
}
}
});
t.setDaemon(true);
t.start();
Thread.currentThread().sleep(3);
for (String s : list) {
System.out.println(s);
}
}
}
明顯的ArrayList丟擲了經典的ConcurrentModificationException異常,這點無需贅述,重點是來看一下ConcurrentHashMap的控制檯輸出:
913471290
a
-1547941244
b
-1175691689
c
265472996
1
1516781777
2
-297067907
3
367817044
4
1723169514
5
-1090557824
6
1451129615
7
-298489538
8
-24239701
9
1150538634
10
1356089445
11
488770731
這裡有兩個執行緒,第一個執行緒是main函式所在的主執行緒,用來迴圈遍歷該map的內容,另一個執行緒是不斷的向容器中新增自增變數count,從迴圈遍歷輸出的hashcode值可以看到:主執行緒不斷的迴圈遍歷的list並不是同一個list,因為它們的hash值不相同。
需要注意的是:
- 讀的操作是不需要加鎖,但是寫的時候是需要加鎖,當多個執行緒同時進行寫的操作時,只有噹噹前執行緒寫操作結束之後才能釋放鎖給其他執行緒使用,所以COW使用於讀多寫少的操作。
- COW只能保證結果的一致性,不能保證操作過程中資料的一致性。
- COW很好的解決併發效能的同時,可能耗費了一定的記憶體,因為在add操作中開闢了一段空間和儲存原來的副本。