多线程环境下同步FixedSizeLinkedHashMap超最大容量问题排查
需要一个最大元素个数为10000的映射,插入新记录时自动移除最旧条目,用于多线程场景。但实际运行中发现元素数量超过10000(甚至达到20000+),以下是实现代码:
public class FixedSizeLinkedHashMap<K, V> extends LinkedHashMap<K, V> { private final int maxSize; public FixedSizeLinkedHashMap(int maxSize) { super(maxSize, 0.75f, true); this.maxSize = maxSize; } @Override protected synchronized boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxSize; } @Override public synchronized V put(K key, V value) { return super.put(key, value); } @Override public synchronized V get(Object key) { return super.get(key); } @Override public synchronized V remove(Object key) { return super.remove(key); } @Override public synchronized void clear() { super.clear(); } }
调用代码:
fixedsizeLhm = new FixedSizeLinkedHashMap<>(10000);
核心问题1:初始容量参数理解错误
LinkedHashMap构造函数的第一个参数是底层数组的初始容量,不是你要限制的最大元素个数。你现在传入10000,结合0.75的负载因子,扩容阈值是10000 * 0.75 = 7500——当元素个数达到7500时,HashMap就会自动扩容,底层数组容量直接翻倍到20000。这就是你看到“容量超过10000”的原因(实际是底层数组容量,而非元素个数)。
修复方式:根据最大元素个数和负载因子计算初始容量,确保扩容阈值大于等于maxSize,避免提前扩容。修改构造函数:
public FixedSizeLinkedHashMap(int maxSize) { // 计算初始容量:确保扩容阈值 >= maxSize int initialCapacity = (int) Math.ceil(maxSize / 0.75f) + 1; super(initialCapacity, 0.75f, true); this.maxSize = maxSize; }
核心问题2:未覆盖所有线程不安全方法
你只给put、get等少数方法加了synchronized,但LinkedHashMap还有putAll、containsKey、containsValue、entrySet等方法没有同步。多线程下调用这些未同步的方法,会导致并发冲突,可能出现元素个数超过maxSize的情况。
修复方式:覆盖所有可能被调用的方法,添加synchronized修饰,比如:
@Override public synchronized void putAll(Map<? extends K, ? extends V> m) { super.putAll(m); } @Override public synchronized boolean containsKey(Object key) { return super.containsKey(key); } @Override public synchronized boolean containsValue(Object value) { return super.containsValue(value); } // 其他如entrySet、keySet、values等方法也建议同步,避免迭代时的并发修改问题 @Override public synchronized Set<Map.Entry<K, V>> entrySet() { return Collections.unmodifiableSet(super.entrySet()); }
次要问题:removeEldestEntry的同步多余
put方法已经加了synchronized,removeEldestEntry是在put的同步块内被调用的,不需要额外给它加synchronized修饰,直接去掉即可:
@Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxSize; }
额外建议:考虑使用ConcurrentLinkedHashMap
如果是高并发场景,自己实现同步的LinkedHashMap性能可能不够理想,可以考虑使用com.googlecode.concurrentlinkedhashmap:concurrentlinkedhashmap-lru库,它是专门为并发场景设计的LRU缓存实现,不需要自己处理同步问题。
内容的提问来源于stack exchange,提问作者user3201343

