实现一个线程安全并且可以设置过期时间的LRU(LinkedHashMap原理)

本文介绍了LRU缓存的原理,重点讲解了如何使用LinkedHashMap实现线程安全且带有过期时间的LRU缓存。通过设置accessOrder为true实现访问顺序存储,利用removeEldestEntry方法处理缓存淘汰,同时通过ReentrantLock解决并发问题,并使用ScheduledThreadPoolExecutor进行定期删除过期缓存。

目录

1、HashMap原理

2、LinkedHashMap实现LRU原理(accessOrder = true)

2.1 数据结构

2.2 put方法

2.3 get方法

2.4 remove方法

3、普通LRU代码实现

4、实现一个线程安全并且可以设置过期时间的LRU缓存

4.1 解决安全问题

4.2 实现定期删除


FIFO的思想是实现一个先进先出的队列,LRU最近最久未使用。可以用双向链表linkedList来实现,同时为了兼顾查询节点时的效率,结合HashMap来实现。双向链表linkedList+HashMap的数据结构可以联想到LinkedHashMap,就不需要我们自己来实现了。LinkedHashMap存储数据是有序的,可以分为插入顺序(accessOrder = false)和访问顺序(accessOrder = true),默认为插入顺序,而且LinkedHashMap提供了删除最后一个节点的方法removeEldestEntry(Map.Entry eldest),正好可以用来实现FIFO(LinkedHashMap按插入顺序存储数据)和LRU算法(LinkedHashMap按访问顺序存储数据)。

1、HashMap原理

底层是Entry数组+链表(Entry节点的next指针)+红黑树。JDK8中,链表长度不小于8时,将链表转化为红黑树。默认无参构造函数会初始化大小为16,向集合中添加元素至集合大小的0.75倍时,会生成一个大小为原来2倍的新集合,然后重新计算元素的地址,将集合中元素插入到新集合,届时效率很低。线程不安全。(例如:put的时候导致的数据覆盖、集合扩展时(resize方法)会出现死循环)。

//HashMap的Entry结构:
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

2、LinkedHashMap实现LRU原理(accessOrder = true

  • 2.1 数据结构

HashMap的原理是内部维护了一个Entry数组,而LinkedHashMap在HashMap的基础上,增加了链表头节点和尾节点两个指针,增加了排序方式的标志,Entry节点增加了前后两个指针。因此LinkedHashMap的Entry节点有三个指针,一个是双向链表的前指针、一个是双向链表的后指针、一个是HashMap的hash地址重复时拉链法解决冲突的next的指针。

/*LinkedHashMap的Entry结构:*/
//双向链表头结点
transient LinkedHashMap.Entry<K,V> head;
//双向链表尾节点
transient LinkedHashMap.Entry<K,V> tail;
//是否基于访问顺序排序(默认为false即插入顺序排序)
final boolean accessOrder;
//Entry继承了HashMap的Entry,又增加了before, after两个指针
private static class Entry<K,V> extends HashMap.Entry<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
            super(hash, key, value, next);
        }
}
  • 2.2 put方法

如果put的是新key,则将Entry节点添加到Map中,并添加到双向链表的尾部,若initialCapacity数量已满,删除最近最久未使用的Entry节点即双向链表的头结点;若put的是已有的key,更新节点的value,并将节点删除并添加到尾部。

HashMap的put方法会生成一个节点,调用了newNode方法,而LinkedHashMap重写了此方法
 /**
     * 创建一个节点
     * @param hash  hash值
     * @param key   键
     * @param value 值
     * @param e     下一个节点,这个是HashMap节点的属性
    
<think> 首先,用户的问题是:“我想查找使用Java实现支持LRU、并发安全和过期策略的高性能本地缓存的方法”。用户提供了几个引用,我需要参考这些引用并结合系统级指令来回答。 回顾系统级指令: 1. 所有行内数学表达式必须使用 $...$ 格式。 2. 独立公式必须使用 $$...$$ 格式并单独成段。 3. LaTeX语法正确。 4. 使用中文回答。 5. 生成相关问题。 6. 回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 用户的问题是关于Java实现支持LRU、并发安全和过期策略的高性能本地缓存。用户提供的引用包括: - [^1]: Java实现LRU缓存的概述。 - [^2]: Java项目中常用LRU缓存实现方式对比,包括LinkedHashMap、Guava和Redis。 - [^3]: Java LRU缓存实现,使用双向链表和哈希表。 - [^4]: Java自定义LRU缓存的线程安全问题。 - [^5]: Java 8实现缓存服务,支持FIFOLRU淘汰策略,并发高速缓存。 我需要整合这些信息,提供一个清晰的、逐步的Java实现方法。用户提到了“高性能本地缓存”,所以我应该聚焦在本地实现,而不是Redis这样的远程缓存。 关键特性: - 支持LRU(最近最少使用)淘汰策略。 - 并发安全:在多线程环境中安全使用。 - 过期策略:数据可以设置过期时间。 - 高性能:高效实现。 从引用中: - [^3] 描述了使用双向链表维护访问顺序 + 哈希表实现快速查询。 - [^4] 提到LinkedHashMap实现不是线程安全的,可以使用Collections.synchronizedMap来包装,但遍历时需要同步。 - [^5] 提到Java 8实现支持LRUFIFO的并发高速缓存。 最佳实践: 1. **基于LinkedHashMap实现LRU**:Java的LinkedHashMap可以通过重写removeEldestEntry方法来实现LRU策略。但这不是线程安全的。 2. **使用Guava Cache**:引用[^2]提到Guava包下的Cache,它是一个流行的缓存库,支持LRU、过期策略和并发安全。 3. **自定义实现**:如[^3]所示,使用双向链表和HashMap可以实现LRU。结合ConcurrentHashMap可以增强并发性。 为了高性能和并发安全: - 使用ConcurrentHashMap作为底层存储。 - 使用一个数据结构(如双向链表)来维护访问顺序。 - 实现线程安全的过期策略。 步骤来实现: 1. **数据结构**:使用ConcurrentHashMap来存储键值对。为了LRU,使用一个双向链表来记录访问顺序。 2. **并发控制**:使用ReentrantLock或synchronized块来同步链表操作。 3. **过期策略**:可以为每个条目添加过期时间,并在get或put时检查是否过期。 4. **淘汰策略**:当缓存满或条目过期时,移除最近最少使用的条目。 高性能考虑: - 最小化锁竞争。 - 使用高效的算法。 既然用户问“方法”,我应该提供代码示例或伪代码。 现在,构建回答结构: - **引言**:简要介绍实现支持LRU、并发安全和过期策略的高性能本地缓存的方法。 - **核心实现步骤**:逐步描述如何实现。 - 使用双向链表和HashMap for LRU。 - 添加并发安全。 - 添加过期策略。 - **代码示例**:提供Java代码片段。 - **替代方案**:提到使用Guava Cache或Caffeine库。 - **相关问题**:生成相关问题。 生成相关问题: - 根据系统指令,需要在末尾生成相关问题。 使用中文回答。 参考引用: - 在回答中自然地添加引用标识,如[^3]提到双向链表实现。 LaTeX:用户的问题没有数学内容,所以可能不需要数学表达式。但如果有,确保格式正确。例如,如果提到算法复杂度,使用$O(1)$等。 最终回答大纲: 1. 介绍实现方法。 2. 解释数据结构。 3. 描述并发安全实现。 4. 添加过期策略。 5. 提供代码示例。 6. 建议使用库如Guava或Caffeine。 7. 生成相关问题。 </think> ### Java实现支持LRU、并发安全和过期策略的高性能本地缓存方法 在Java中实现一个支持LRU(最近最少使用)淘汰策略、并发安全(线程安全)和过期策略的高性能本地缓存,需要结合高效的数据结构、并发控制机制和时间管理策略。以下方法基于引用内容整合最佳实践,逐步实现核心功能。核心思想是:使用哈希表实现快速查询、双向链表维护访问顺序(实现LRU)、添加过期时间戳,并通过精细的锁策略确保并发安全。整体设计目标为 $O(1)$ 的平均时间复杂度用于get和put操作[^3]。下面逐步说明实现方法。 #### 1. **核心数据结构选择** - **哈希表**:使用 `ConcurrentHashMap` 作为基础存储,实现高并发访问和快速键值查找($O(1)$ 复杂度)[^3][^5]。 - **双向链表**:维护条目访问顺序(最近访问的移到链表头部,最少使用的在尾部),用于实现LRU策略。链表的每个节点存储键、值、创建时间和过期时间[^3]。 - **节点设计**:每个缓存条目封装为节点对象,包含: - `key`:条目键。 - `value`:条目值。 - `createTime`:条目创建时间戳(用于计算过期)。 - `expireTime`:条目过期时间(单位毫秒)。 - 淘汰触发:当缓存满或条目过期时,优先移除链表尾部的节点(LRU策略)。 #### 2. **实现并发安全** - **锁机制**:使用 `ReentrantLock` 或 `synchronized` 块同步链表操作(如添加、删除、访问顺序更新)。避免使用全局锁以减少竞争: - HashMap部分由 `ConcurrentHashMap` 处理,确保线程安全。 - 链表操作需要自定义锁,保护链表结构的并发修改[^4]。 - **注意事项**: - 直接使用 `Collections.synchronizedMap` 包装链表结构可能导致遍历不安全(如引用[^4]所述),建议显式控制锁范围。 - 对于高并发场景,使用读写锁 (`ReentrantReadWriteLock`) 优化:读操作共享锁,写操作独占锁。 #### 3. **添加过期策略** - **过期检查**:为每个条目添加 `expireTime`属性。在get或put操作时自动检查: - 如果当前时间大于 `expireTime`,条目被视为过期,触发移除。 - 惰性删除:仅在访问时检查过期,减少开销;也可后台线程定期扫描清理。 - **时间管理**:使用 `System.currentTimeMillis()` 获取时间戳,避免 `new Date()` 开销。 #### 4. **高性能优化** - **LRU维护**:每次访问条目(get或put)时更新链表顺序: - 访问条目后,将其移到链表头部。 - 当缓存满时(容量上限),移除尾部条目。 - **内存控制**:初始化时设置固定容量(如从属性文件配置),超出时触发LRU淘汰[^5]。 - **复杂度保证**:哈希表操作 $O(1)$,链表顺序更新 $O(1)$,确保整体高性能。 - **避免阻塞**:异步处理过期清理(如使用 `ScheduledExecutorService`)。 #### 5. **完整Java实现示例** 以下是一个精简版并发安全LRU缓存实现,支持过期策略。代码使用Java 8+特性,包括 `ConcurrentHashMap` 和 `ReentrantLock`。 ```java import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; public class LRUCache<K, V> { // 数据结构定义 private static class Node<K, V> { K key; V value; long createTime; // 创建时间戳 long expireTime; // 过期时间(毫秒),-1表示永不过期 Node<K, V> prev; Node<K, V> next; Node(K key, V value, long expireTime) { this.key = key; this.value = value; this.createTime = System.currentTimeMillis(); this.expireTime = expireTime; } boolean isExpired() { return expireTime >= 0 && System.currentTimeMillis() > createTime + expireTime; } } // 缓存配置 private final int capacity; private final Map<K, Node<K, V>> cacheMap; // 哈希表存储 private final Node<K, V> head; // 链表头(最近使用) private final Node<K, V> tail; // 链表尾(最少使用) private final ReentrantLock lock = new ReentrantLock(); // 链表操作锁 public LRUCache(int capacity) { this.capacity = capacity; this.cacheMap = new ConcurrentHashMap<>(); head = new Node<>(null, null, -1); tail = new Node<>(null, null, -1); head.next = tail; tail.prev = head; } // Get操作:获取值并更新访问顺序 public V get(K key) { Node<K, V> node = cacheMap.get(key); if (node == null) return null; lock.lock(); try { if (node.isExpired()) { removeNode(node); // 过期移除 cacheMap.remove(key); return null; } moveToHead(node); // 移到链表头部(最近使用) return node.value; } finally { lock.unlock(); } } // Put操作:添加或更新条目 public void put(K key, V value, long expireTime) { lock.lock(); try { Node<K, V> node = cacheMap.get(key); if (node != null) { node.value = value; // 更新值 node.expireTime = expireTime; // 更新过期时间 moveToHead(node); // 移到头部 } else { if (cacheMap.size() >= capacity) { removeTail(); // 移除尾部(LRU淘汰) } Node<K, V> newNode = new Node<>(key, value, expireTime); cacheMap.put(key, newNode); addToHead(newNode); // 添加新节点到头部 } } finally { lock.unlock(); } } // 辅助方法:添加节点到链表头部 private void addToHead(Node<K, V> node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } // 辅助方法:移动节点到头部 private void moveToHead(Node<K, V> node) { removeNode(node); addToHead(node); } // 辅助方法:移除节点 private void removeNode(Node<K, V> node) { node.prev.next = node.next; node.next.prev = node.prev; } // 辅助方法:移除尾部节点(LRU淘汰) private void removeTail() { Node<K, V> node = tail.prev; if (node != head) { removeNode(node); cacheMap.remove(node.key); } } } ``` #### 关键说明: - **并发安全**:使用 `ReentrantLock` 保护链表操作,`ConcurrentHashMap` 确保哈希表线程安全。避免遍历时并发问题(不同于引用[^4]的 `Collections.synchronizedMap` 限制)。 - **过期策略**:`isExpired()` 方法在get时自动检查并移除过期条目。可扩展为后台线程定时清理。 - **LRU实现**:通过双向链表维护访问顺序,确保淘汰策略高效。 - **高性能**:整体操作时间复杂度 $O(1)$,适用于高频访问场景。 - **容量配置**:通过构造函数设置缓存大小(如 `new LRUCache(1000)`),支持从属性文件加载[^5]。 #### 推荐替代方案 - **使用成熟库**:对于生产环境,推荐基于引用[^2][^5]使用Guava Cache或Caffeine库,它们内置LRU、过期策略和并发优化。例如: - Guava Cache: `CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES).build()`。 - Caffeine: 更高性能,支持异步刷新。 - **为何自定义实现?** 当需要高度定制化时(如特定淘汰逻辑),自定义实现更灵活;否则优先使用库以简化开发[^2]。 此方法平衡了性能、并发性和功能完整性,适用于本地缓存场景如Web会话管理或数据缓存[^1][^3]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值