1. 程式人生 > >高併發程式設計:初識併發容器類

高併發程式設計:初識併發容器類

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值不相同。

需要注意的是:

  1. 讀的操作是不需要加鎖,但是寫的時候是需要加鎖,當多個執行緒同時進行寫的操作時,只有噹噹前執行緒寫操作結束之後才能釋放鎖給其他執行緒使用,所以COW使用於讀多寫少的操作。
  2. COW只能保證結果的一致性,不能保證操作過程中資料的一致性。
  3. COW很好的解決併發效能的同時,可能耗費了一定的記憶體,因為在add操作中開闢了一段空間和儲存原來的副本。