面試題18解析-同步容器

編程語言 Java Java虛擬機 ESET Java面試那些事兒 2017-06-11

面試題18解析-同步容器

題目:

請說明一下同步容器CopyOnWriteArrayList/ConcurrentHashMap/SynchronizedMap(Collections定義的內部類)的區別及它們的應用場景?

在Java多線程開發中,往往需要java中常用的容器作為共享資源來使用,如Array、List、Hashmap、Set等。但是基本的容器在多線程下進行併發操作是有問題,需要進行加鎖才能保證多線程共享容器的線程安全(Thread Safe)。為了簡化java多線程開發,JDK中提供了一些線程安全容器,使得程序員在開發多線程應用時,可以將更多的精力放在程序邏輯上,而不是錯綜複雜的鎖處理上。但JDK中不同的併發容器類,擁有不同的性質和應用場景,這裡我們就來分析一下三種線程安全的併發容器:ConcurrentHashMap、SychronizedMap和CopyOnWriteArrayList。

三種容器的併發說明

首先說明一下這三種容器代表的是三類容器,我們這裡只討論這三類容器在應對併發處理上區別,並不討論容器的數據結構區別,例如ConcurrentHashMap和ConcurrentLinkedQueue是一類容器,SychronizedMap、SynchronizedList和SynchronizedSet是一類容器。在同一類併發容器中,其同步處理策略基本上是相同的,我們在掌握其中一種容器並法特性後,便可掌握了這一類的併發容器的特性,只需在實際應用中挑選合適的數據結構即可。

SychronizedMap

SychronizedMap是Collections包提供的一種構造安全Map容器的方法,通過靜態方法Collections.synchronizedMap()便可構造一個安全容器。我們通過源碼來看一下Collections是如何構造安全容器的,下面是SynchronizedMap的實現代碼(JDK8):

private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
... \\這裡省略
}

我們可以看到SynchronizedMap使用synchronized對Map的所有操作都進行了加鎖,從而保證了Map的線程安全性。SynchronizedMap雖然通過加鎖保證了Map的線程安全性,但是整個Map只使用了一把獨佔鎖,會造成了同一時間只有一個線程可以獲取鎖,對Map進行操作。這樣Map的併發性就會很差,換句話說SynchronizedMap的併發量其實只有1。為了解決這樣的問題,java併發大師Doug Lea開發了性能更好的ConcurrentHashMap類。

ConcurrentHashMap

為了避免在多線程環境下競爭一把鎖而造成的性能瓶頸問題,ConcurrentHashMap使用了鎖分段技術。ConcurrentHashMap將容器中的數據進行了分段(segment),並且每一個段擁有一把鎖(ReentrantLock),這樣只有多線程在同時訪問到同一數據段中的元素(HashEntry)時,才會存在鎖競爭問題,這樣就大大減少了線程阻塞在同一把鎖的概率,從而提高了性能。下圖便是ConcurrentHashMap的結構圖:

面試題18解析-同步容器

在JDK 7中Segment類的實現如下:

static class Segment<K,V> extends ReentrantLock implements Serializable {
...
}

可以看出Segment繼承了ReentrantLock重入鎖,可以當作鎖來用,因此對於HashEntry的同步操作都依賴於其對應的段的Segment鎖。這裡需要說明一下,由於HashMap在JDK8中有重大改變,增加了紅黑樹,其對應的ConcurrentHashMap也不再使用段鎖機制來保證容器的線程安全了。在JDK 8中關於Segment添加了這樣的說明:

/* Stripped-down version of helper class used in previous version,declared for the sake of serialization compatibility*/

也就是Segment類的聲明只為之前版本序列化兼容性操作,雖然JDK8不再使用段鎖機制,但是作者認為段鎖機制是一種值得我們學習的併發控制思想,在我們的實際開發中,常常不考慮程序併發性能,遇到多線程就上synchronized鎖,從頭鎖到尾,效率非常低下,這樣的代碼可以說是偽多線程代碼,說必定還不如單線程的性能高(畢竟線程切換也是消耗性能的),因此大師級的併發實現必然很值得我們學習。在JDK8中的ConcurrentHashMap是使用了synchronized對Node(樹節點)進行加鎖操作,這裡就不深入進行討論了,只需讀者記住JDK8已經重新實現了ConcurrentHashMap,如果作者以後有機會寫HashMap或者紅黑樹相關的博文時,會深入分析ConcurrentHashMap的實現的。

CopyOnWriteArrayList

Copy-On-Write寫時複製,這又是一種提高系統效率的編程思想。其思想是,在程序正常運行時所有讀操作,都基於同一容器進行讀操作,在容器進行寫操作時,不是通過加鎖來控制線程的寫鎖獲取,而是先將容器完全複製出來一份,在新的容器上進行寫操作,最後將舊容器的引用指向新容器,這樣就完成了新容器的寫操作。CopyOnWrite容器與讀寫鎖(ReentrantReadWriteLock)的相同性質,進行了讀寫區別對待,只在寫時加鎖,從而提高了容器的性能。CopyOnWriteArrayList.set()實現如下(JDK8):

public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}

從set()方法的實現中,可以發現在更改CopyOnWriteArrayList中的元素時,CopyOnWriteArrayList會將原數組elements進行完全複製,並且更新元素是在新生成的newElements數組的上進行更新,最後再通過setArray()方法將新數組賦值給原數組的引用。而在CopyOnWriteArrayList的讀操作中,並沒有加鎖:

private E get(Object[] a, int index) {
return (E) a[index];
}

通過上述分析,我們可以總結出CopyOnWrite類容器具有以下性質(優缺點):

  1. 讀操作沒有加鎖,讀操作不會存在線程阻塞等待現象。

  2. 寫操作會複製整個容器,有可能造成內存大幅增長,使用不當會導致java虛擬機頻繁FullGC()。

  3. 讀操作不能立即可見。由於寫操作是在新數組上進行的,因此新元素不可能對在舊數組上進行讀操作的線程可見。

因此,CopyOnWrite在實際開發中,適合在讀操作頻繁,容器元素穩定的生產環境中使用,並且一定要注意容器大小的控制,頻繁的寫操作會造成大內存的頻繁申請與釋放,有可能因此觸發java虛擬機的stop-the-world。

小結

本文分析了三種類型的併發容器,在實際使用中如果不是JDK版本的限制,請CucurrentHashMap來替代SychronizedMap。而CopyOnWrite類容器則一般用在容器較為穩定,讀操作遠比寫操作頻繁的場景中。

此外說句題外話,我們在學習使用這些併發容器的過程中,不因僅僅學習其用法,更應該通過學習其大師級的設計思想,以及實現方式,來掌握同步控制的技巧,做到觸類旁通,舉一反三。

記得關注我們哦,這裡全部都是乾貨!!!

相關推薦

推薦中...