在開發中,我習慣使用 ConcurrentHashMap 做緩存,但當需要控制緩存大小並進行緩存淘汰時,我通常依賴第三方緩存框架,如 Caffeine 或 Guava Cache。
最近在閲讀 Mondrian 源碼時,發現了一種新的緩存設計,引發了我的思考,特此記錄。
在 Mondrian 源碼中,有這樣一段代碼:
private final Map<Integer, MutableConnectionInfo> connectionMap =
new LinkedHashMap<Integer, MutableConnectionInfo>( MondrianProperties.instance().ExecutionHistorySize.get(),
0.8f, false ) {
private final int maxSize = MondrianProperties.instance().ExecutionHistorySize.get();
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry( Map.Entry<Integer, MutableConnectionInfo> e ) {
if ( size() > maxSize ) {
if ( RolapUtil.MONITOR_LOGGER.isTraceEnabled() ) {
RolapUtil.MONITOR_LOGGER.trace( "ConnectionInfo(" + e.getKey() + ") evicted. Stack is:" + Util.nl + e
.getValue().stack );
}
return true;
}
return false;
}
};
雖然寫了多年的 Java,但很少使用LinkedHashMap。看到 Mondrian 使用LinkedHashMap的匿名子類並重寫了removeEldestEntry方法,我特意查了一下這個API的作用。
在LinkedHashMap中,removeEldestEntry方法會在插入元素時自動調用。如果該方法返回 true,則在保存新元素之前刪除鏈表頭部的元素。然而LinkedHashMap中removeEldestEntr的默認實現為 false,即不會刪除元素。
Mondrian 實現了LinkedHashMap的匿名子類並重寫了removeEldestEntry方法,主要目的是限制 connectionMap緩存的大小。當connectionMap中的元素超過指定大小時,removeEldestEntry返回true,刪除鏈表頭部的元素,從而保持connectionMap的尺寸。
Mondrian 實現了LinkedHashMap的匿名子類並重寫了removeEldestEntry方法,主要目的是限制 connectionMap緩存的大小。當connectionMap中的元素超過指定大小時,removeEldestEntry 返回 true,刪除鏈表頭部的元素,從而保持 connectionMap 的尺寸。
於是有了以下思考:以後設計公共緩存的時候能不能使用LinkedHashMap來代替以前常用的ConcurrentHashMap?
方法1:使用 Collections.synchronizedMap包裝LinkedHashMap
Collections.synchronizedMap可以將任意Map包裝為線程安全的Map。但在高併發的情況下存在性能瓶頸,因為它採用的是同步鎖。
public class ThreadSafeLRUCache<K, V> {
private final Map<K, V> cache;
public ThreadSafeLRUCache(final int maxSize) {
this.cache = Collections.synchronizedMap(new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
});
}
public V get(K key) {
return cache.get(key);
}
public void put(K key, V value) {
cache.put(key, value);
}
public int size() {
return cache.size();
}
}
方法2:使用ReadWriteLock和LinkedHashMap
ReadWriteLock 比同步鎖 (synchronized) 更適合高併發訪問,因為它提供了更細粒度的鎖控制,特別是在讀多寫少的情況下。
ReadWriteLock 提供了2種鎖:
1、讀鎖(Read Lock):允許多個線程同時讀取資源。
2、寫鎖(Write Lock):獨佔鎖,只允許一個線程獲取寫鎖,同時禁止其他線程獲取讀鎖和寫鎖。
Public class ReadWriteLockLRUCache<K, V> {
private final Map<K, V> cache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public ReadWriteLockLRUCache(final int maxSize) {
this.cache = new LinkedHashMap<K, V>(maxSize, 0.75f, true) {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
};
}
public V get(K key) {
lock.readLock().lock();
try {
return cache.get(key);
} finally {
lock.readLock().unlock();
}
}
public void put(K key, V value) {
lock.writeLock().lock();
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock();
}
}
public int size() {
lock.readLock().lock();
try {
return cache.size();
} finally {
lock.readLock().unlock();
}
}
}
特別説明
在 Mondrian 源碼中,LinkedHashMap構造函數的第三個參數是 false,表示按插入順序排序(FIFO)。而在我思考的代碼中使用的是 true,表示按訪問順序排序(LRU)。因此:
false:按插入順序排序,實現 FIFO(先進先出)策略。
true:按訪問順序排序,實現 LRU(最近最少使用)策略。
總結
通過對 Mondrian 源碼的學習,我發現了使用LinkedHashMap實現緩存淘汰策略的優雅方法。這種方法不僅可以控制緩存的大小,還能根據需要自定義淘汰策略。相比之下,傳統的ConcurrentHashMap 無法直接提供這些功能。
結合上述兩種實現方案,Collections.synchronizedMap 和 ReadWriteLock,我們可以在不同的併發需求下選擇合適的緩存實現。Collections.synchronizedMap 適用於併發量較低的場景,而 ReadWriteLock則更適合讀多寫少的高併發環境。
總之,合理選擇和設計緩存策略,可以顯著提升系統的性能和可維護性。在以後的開發中,可以考慮根據具體需求使用LinkedHashMap實現更靈活高效的緩存機制。