头图

在开发中,我习惯使用 ConcurrentHashMap 做缓存,但当需要控制缓存大小并进行缓存淘汰时,我通常依赖第三方缓存框架,如 CaffeineGuava 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,则在保存新元素之前删除链表头部的元素。然而LinkedHashMapremoveEldestEntr的默认实现为 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:使用ReadWriteLockLinkedHashMap
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.synchronizedMapReadWriteLock,我们可以在不同的并发需求下选择合适的缓存实现。Collections.synchronizedMap 适用于并发量较低的场景,而 ReadWriteLock则更适合读多写少的高并发环境。

总之,合理选择和设计缓存策略,可以显著提升系统的性能和可维护性。在以后的开发中,可以考虑根据具体需求使用LinkedHashMap实现更灵活高效的缓存机制。


抓bug的猫
179 声望30 粉丝

优雅永不过时!