最近調優(yōu)一個多線程使用共享Map對象本地緩存性能問題。原有實現(xiàn)背景為Map對象存儲從Redis加載的數(shù)據(jù),如果對應Redis數(shù)據(jù)為空,需要調用Redis加載邏輯,這段邏輯封裝在一個更新數(shù)據(jù)方法,并且加了同步鎖,實現(xiàn)線程安全。
示例代碼:private Map<String,Object> cachMap = Maps.newHashMap();public synchronized void updateCache(Map<String,Object> getNewMap){ cachMap.clear(); cachMap.putAll(getNewMap); } public synchronized Map<String,Object> getCache(){ return cachMap; }
我們都知道synchronized是實現(xiàn)共享對象同步處理的首選解決方法,在處理線程較少資源競爭下情況下確實性能不會有太大影響,但是現(xiàn)在我們面臨的場景是讀取cache的請求增加了10倍左右,寫入數(shù)據(jù)操作基本維持不變,應用性能明顯下降,排查發(fā)現(xiàn)synchronized是問題之一,因為它導致我們的cache訪問變成串行,影響整體鏈路并發(fā)執(zhí)行效率。
private ReadWriteLock cacheRWLock = new ReentrantReadWriteLock(); public void updateCache(Map<String,Object> getNewMap){ try { cacheRWLock.writeLock().lock();//獲取寫鎖,唯一一個線程持有 cachMap.clear(); cachMap.putAll(getNewMap); } catch (Exception e) { // TODO: handle exception }finally{ cacheRWLock.writeLock().unlock();//釋放寫鎖 } } public Map<String,Object> getCache(){ try { cacheRWLock.readLock().lock();//獲取讀鎖,多個線程可共享 return cachMap; } catch (Exception e) { // TODO: handle exception }finally{ cacheRWLock.readLock().unlock();//釋放讀鎖,以免影響寫鎖 } return null; }
分析代碼示例,變更共享對象Map前獲取寫鎖,如果獲取不到線程等待。獲取到寫鎖后 開始數(shù)據(jù)操作,數(shù)據(jù)操作之后要釋放寫鎖。而讀取對象Map前獲取讀鎖,同一時刻可以有多個線程獲取讀鎖從而達到并行的目的。對于同一個線程,如果獲取了寫鎖,同樣可以獲取讀鎖,也就是API描述的可重入性。讀鎖和寫鎖在ReadWriteLock 采用了不同的鎖機制,具體原理不表,API有詳細描述。
2. 將HashMap替換為ConcurrentHashMap,實現(xiàn)細粒度線程安全同時提升并發(fā)性能。
從場景中分析可知,我們需要處理的共享對象是HashMap實例,HashMap類在并發(fā)場景下非線程安全。但util.concurrent提供了線程安全的ConcurrentHashMap實現(xiàn),這個類的實現(xiàn)采用了是細粒度的hash segment維度的同步機制,也就是不同的segment對應不同的鎖,通過這種方式可以保持同步互斥下的并發(fā)處理能力。具體我們分析下ConcurrentHashMap的clear和putAll實現(xiàn)源碼。
public void clear() { final Segment<K,V>[] segments = this.segments; for (int j = 0; j < segments.length; ++j) { Segment<K,V> s = segmentAt(segments, j); if (s != null) s.clear(); }}static final <K,V> Segment<K,V> segmentAt(Segment<K,V>[] ss, int j) { long u = (j << SSHIFT) + SBASE; return ss == null ? null : (Segment<K,V>) UNSAFE.getObjectVolatile(ss, u); }
Clear的實現(xiàn)過程是遍歷map對象所有的segments,針對每個segment持有的volatile 定義的鎖對象判斷是否有同步鎖,如果都沒命中可以執(zhí)行清理。
public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject (segments, (j << SSHIFT) + SBASE)) == null) s = ensureSegment(j); return s.put(key, hash, value, false); }
PutAll的實現(xiàn)基礎是put方法,基本原理是針對傳入數(shù)據(jù)key做hash,然后針對命中的segment判斷是否能獲取同步鎖,如果能獲取分配segments寫入,否則等待。
對比兩種方案性能總體相差不大,測試case不表。
對于使用讀寫鎖優(yōu)化并發(fā)性能場景基于讀取量大于變更量的前提,如果讀取量跟變更量基本持平情況下就需要根據(jù)業(yè)務具體情況采用相應的策略,那有沒有通用的解決方案?
這種除了沿用Syncronized關鍵字實現(xiàn)重量級鎖,也可以采用volatile 實現(xiàn)內存共享實例,但是頻繁操作內存的成本較高不太適合。那是否可以結合volatile 和 ReentrantLock 實踐CopyOnWrite機制,實現(xiàn)CopyOnWriteArrayList相似功能(java.util.concurrent 沒有實現(xiàn)CopyOnWriteMap類,一直比較費解)。CopyOnWriteMap一種實現(xiàn)方式(源碼來源mongo-java-driver)可以拿出來分析一下,假設需要實現(xiàn)場景需要的clear和putAll方法代碼如下:
private volatile M delegate;// volatile定義的同步Map實例private final transient Lock lock = new ReentrantLock(); //定義讀寫鎖
基于上述兩個關鍵同步變量定義,clear方法優(yōu)先獲取鎖,然后采用置換方法,把空Map對象置換變更delegate存儲的Map,基于volatile導致當前線程把數(shù)據(jù)從緩存中寫入內存中導致其他線程緩存失效的特性,保持數(shù)據(jù)變更的同步。
public final void clear() { lock.lock(); try { set(copy(Collections.<K, V> emptyMap())); } finally { lock.unlock(); } }
PutAll方法通用是獲取讀寫鎖,然后將寫入delegate存儲的舊Map對象copy出來,同時把新傳入的對象copy到中間Map對象,最后將中間Map對象寫入delegate。對應的delegate同步實現(xiàn)也交給JVM完成。操作完成后釋放讀寫鎖。
public final void putAll(final Map<? extends K, ? extends V> t) { lock.lock(); try { final M map = copy(); map.putAll(t); set(map); } finally { lock.unlock(); } }
從這個實現(xiàn)分析,CopyOnWriteMap非常依賴volatile存儲的對象,而我們的緩存肯定是有限制的,所以這種方案存在一定局限性:需要限制處理Map大小,在使用前需要評估預估存儲量,否則過大map容易導致內存溢出。