在开发中,我习惯使用 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
实现更灵活高效的缓存机制。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。