1. 程式人生 > >高併發下的Java資料結構(List,Set,Map,Queue)

高併發下的Java資料結構(List,Set,Map,Queue)

由於並行程式與序列程式的不同特點,適用於序列程式的一些資料結構可能無法直接在併發環境下正常工作,這是因為這些資料結構不是執行緒安全的。本節將著重介紹一些可以用於多執行緒環境的資料結構,如併發List、併發Set、併發Map等。

1.併發List

Vector 或者 CopyOnWriteArrayList 是兩個執行緒安全的List實現,ArrayList 不是執行緒安全的。因此,應該儘量避免在多執行緒環境中使用ArrayList。如果因為某些原因必須使用的,則需要使用Collections.synchronizedList(List list)進行包裝。

示例程式碼:

        List list = Collections.synchronizedList(new ArrayList());
            ...
        synchronized (list) {
            Iterator i = list.iterator(); // 必須在同步塊中
            while (i.hasNext())
                foo(i.next());
        }

CopyOnWriteArrayList 的內部實現與Vector又有所不同。顧名思義,Copy-On-Write 就是 CopyOnWriteArrayList 的實現機制。即當物件進行寫操作時,複製該物件;若進行的讀操作,則直接返回結果,操作過程中不需要進行同步。

CopyOnWriteArrayList 很好地利用了物件的不變性,在沒有對物件進行寫操作前,由於物件未發生改變,因此不需要加鎖。而在試圖改變物件時,總是先獲取物件的一個副本,然後對副本進行修改,最後將副本寫回。

這種實現方式的核心思想是減少鎖競爭,從而提高在高併發時的讀取效能,但是它卻在一定程度上犧牲了寫的效能。

在 get() 操作上,Vector 使用了同步關鍵字,所有的 get() 操作都必須先取得物件鎖才能進行。在高併發的情況下,大量的鎖競爭會拖累系統性能。反觀CopyOnWriteArrayList 的get() 實現,並沒有任何的鎖操作。

在 add() 操作上,CopyOnWriteArrayList 的寫操作效能不如Vector,原因也在於Copy-On-Write。

在讀多寫少的高併發環境中,使用 CopyOnWriteArrayList 可以提高系統的效能,但是,在寫多讀少的場合,CopyOnWriteArrayList 的效能可能不如 Vector。

2.併發Set

和List相似,併發Set也有一個 CopyOnWriteArraySet ,它實現了 Set 介面,並且是執行緒安全的。它的內部實現完全依賴於 CopyOnWriteArrayList ,因此,它的特性和 CopyOnWriteArrayList 完全一致,適用於 讀多寫少的高併發場合,在需要併發寫的場合,則可以使用 Set s = Collections.synchronizedSet(Set<T> s)

得到一個執行緒安全的Set。

示例程式碼:

    Set s = Collections.synchronizedSet(new HashSet());
        ...
    synchronized (s) {
        Iterator i = s.iterator(); // 必須在同步塊中
        while (i.hasNext())
            foo(i.next());
    }

3.併發Map

在多執行緒環境下使用Map,一般也可以使用 Collections.synchronizedMap()方法得到一個執行緒安全的 Map(詳見示例程式碼1)。但是在高併發的情況下,這個Map的效能表現不是最優的。由於 Map 是使用相當頻繁的一個數據結構,因此 JDK 中便提供了一個專用於高併發的 Map 實現 ConcurrentHashMap。

Collections的示例程式碼1:

        Map m = Collections.synchronizedMap(new HashMap());
            ...
        Set s = m.keySet();  // 不需要同步塊
            ...
        synchronized (m) {  // 同步在m上,而不是s上!!
            Iterator i = s.iterator(); // 必須在同步塊中
            while (i.hasNext())
                foo(i.next());
        }

1.為什麼不能在高併發下使用HashMap?

因為多執行緒環境下,使用Hashmap進行put操作會引起死迴圈,導致CPU利用率接近100%,所以在併發情況下不能使用HashMap。

2.為什麼不使用執行緒安全的HashTable?

HashTable容器使用synchronized來保證執行緒安全,但線上程競爭激烈的情況下HashTable的效率非常低下。因為當一個執行緒訪問HashTable的同步方法時,其他執行緒訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如執行緒1使用put進行新增元素,執行緒2不但不能使用put方法新增元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。

3.ConcurrentHashMap的優勢

ConcurrentHashMap的內部實現進行了鎖分離(或鎖分段),所以它的鎖粒度小於同步的 HashMap;同時,ConcurrentHashMap的 get() 操作也是無鎖的。

鎖分離:首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。

4.併發Queue

在併發佇列上,JDK提供了兩套實現,一個是以 ConcurrentLinkedQueue 為代表的高效能佇列,一個是以 BlockingQueue 介面為代表的阻塞佇列。不論哪種實現,都繼承自 Queue 介面。

ConcurrentLinkedQueue 是一個適用於高併發場景下的佇列。它通過無鎖的方式,實現了高併發狀態下的高效能。通常,ConcurrentLinkedQueue 的效能要好於 BlockingQueue 。

與 ConcurrentLinkedQueue 的使用場景不同,BlockingQueue 的主要功能並不是在於提升高併發時的佇列效能,而在於簡化多執行緒間的資料共享。

BlockingQueue 典型的使用場景是生產者-消費者模式,生產者總是將產品放入 BlockingQueue 佇列,而消費者從佇列中取出產品消費,從而實現資料共享。

BlockingQueue 提供一種讀寫阻塞等待的機制,即如果消費者速度較快,則 BlockingQueue 則可能被清空,此時消費執行緒再試圖從 BlockingQueue 讀取資料時就會被阻塞。反之,如果生產執行緒較快,則 BlockingQueue 可能會被裝滿,此時,生產執行緒再試圖向 BlockingQueue 佇列裝入資料時,便會被阻塞等待,其工作模式如圖所示。

5.併發Deque

在JDK1.6中,還提供了一種雙端佇列(Double-Ended Queue),簡稱Deque。Deque允許在佇列的頭部或尾部進行出隊和入隊操作。與Queue相比,具有更加複雜的功能。

Deque 介面的實現類:LinkedList、ArrayDeque和LinkedBlockingDeque。

它們都實現了雙端佇列Deque介面。其中LinkedList使用連結串列實現了雙端佇列,ArrayDeque使用陣列實現雙端佇列。通常情況下,由於ArrayDeque基於陣列實現,擁有高效的隨機訪問效能,因此ArrayDeque具有更好的遍效能。但是當佇列的大小發生變化較大時,ArrayDeque需要重新分配記憶體,並進行陣列複製,在這種環境下,基於連結串列的 LinkedList 沒有記憶體調整和陣列複製的負擔,效能表現會比較好。但無論是LinkedList或是ArrayDeque,它們都不是執行緒安全的。

LinkedBlockingDeque 是一個執行緒安全的雙端佇列實現。可以說,它已經是最為複雜的一個佇列實現。在內部實現中,LinkedBlockingDeque 使用連結串列結構。每一個佇列節點都維護了一個前驅節點和一個後驅節點。LinkedBlockingDeque 沒有進行讀寫鎖的分離,因此同一時間只能有一個執行緒對其進行操作。因此,在高併發應用中,它的效能表現要遠遠低於 LinkedBlockingQueue,更要低於 ConcurrentLinkedQueue 。

參考

《Java程式效能優化》葛一鳴著