高併發下的Java數據結構(List、Set、Map、Queue)

因爲並行程序與串行程序的不一樣特色,適用於串行程序的一些數據結構可能沒法直接在併發環境下正常工做,這是由於這些數據結構不是線程安全的。本節將着重介紹一些能夠用於多線程環境的數據結構,如併發List、併發Set、併發Map等。html

1.併發List

Vector 或者 CopyOnWriteArrayList 是兩個線程安全的List實現,ArrayList 不是線程安全的。所以,應該儘可能避免在多線程環境中使用ArrayList。若是由於某些緣由必須使用的,則須要使用Collections.synchronizedList(List list)進行包裝。java

示例代碼:數組

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。高併發

Copy-On-Write源碼分析

經過查看CopyOnWriteArrayList類的源碼可知,在add操做上,是使用了Lock鎖作了同步處理,內部拷貝了原數組,並在新數組上進行添加操做,最後將新數組替換掉舊數組。源碼分析

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

CopyOnWriteArrayList的get(int index)方法是沒有任何鎖處理的,直接返回數組對象。

public E get(int index) {
        return get(getArray(), index);
    }

    final Object[] getArray() {
        return array;
    }

那麼Copy-On-Write的優缺點有哪些呢?

最明顯的就是這是CopyOnWriteArrayList屬於線程安全的,併發的讀是沒有異常的,讀寫操做被分離。缺點就是在寫入時不止加鎖,還使用了Arrays.copyOf()進行了數組複製,性能開銷較大,遇到大對象也會致使內存佔用較大。

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() 操做也是無鎖的。除非讀到的值是空的纔會加鎖重讀,咱們知道HashTable容器的get方法是須要加鎖的,那麼ConcurrentHashMap的get操做是如何作到不加鎖的呢?緣由是它的get方法裏將要使用的共享變量都定義成volatile。

鎖分離:首先將數據分紅一段一段的存儲,而後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其餘段的數據也能被其餘線程訪問。有些方法須要跨段,好比size()和containsValue(),它們可能須要鎖定整個表而而不只僅是某個段,這須要按順序鎖定全部段,操做完畢後,又按順序釋放全部段的鎖。

上述文字部分參考自文章:Java集合---ConcurrentHashMap原理分析

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程序性能優化》葛一鳴著

相關文章
相關標籤/搜索