LRU缓存设计是一个能够考察许多知识点以及实际编程能力的题目,因为我们在实际工作中是很有可能会去自己写一个LRU算法的简单缓存。本题是LeetCode的第 146 题。LRU——即 Least Recently Used,淘汰最近最少使用的元素的算法。考察的主要内容包括:
- LRU算法的原理与思想
- 具有实际开发意义的编程题
- 线程安全的设计
设计思路
首先考虑简单的设计实现。一个LRU的缓存是基于队列实现的,Java中可以基于LinkedList或者Deque队列实现。需要实现两个基本操作get/put
- 数据结构上,缓存需要指定容量,不能随意扩展。
- 用一个线程安全的ConcurrentHashMap存储数据的键值对。
- 用一个Deque队列表征数据的最近使用频率。
get操作
- 当需要查询的数据已经在缓存中时,返回其值,并将其在队列中的位置重置。
- 若数据不存在返回-1(假设需要缓存的数据均为正数)
put操作
- 当需要put的数据已经存在于缓存中时,更新其在队列的位置,将其放置到队列尾端,表示为最近使用;然后将新的数据put到缓存map中。
- 当需要put的数据不存在于缓存中,且缓存容量达到指定上限时,从队列首移除一个元素,将新元素放置到队尾,再移除map中的数据,更新缓存。
1 public class LRUCache {
2
3 private final int capacity;
4
5 private Map<Integer, Integer> map = new ConcurrentHashMap<>();
6 private Deque<Integer> queue = new LinkedList<Integer>();
7
8 public LRUCache(int capacity) {
9 this.capacity = capacity;
10 }
11
12 public int get(int key) {
13 Integer value = map.get(key);
14 if (value != null) {
15
16 this.queue.remove((Integer)key);
17 this.queue.addLast(key);
18 return value;
19 }
20 return -1;
21 }
22
23 public void put(int key, int value) {
24 if (map.get(key) != null) {
25 this.queue.remove((Integer)key);
26 this.queue.addLast(key);
27 map.put(key, value);
28 return;
29 }
30
31 if (queue.size() >= capacity) {
32 Integer lruKey = this.queue.pollFirst();
33 map.remove(lruKey);
34 }
35 this.queue.addLast(key);
36 map.put(key, value);
37 }
38 }
39
40 /**
41 * Your LRUCache object will be instantiated and called as such:
42 * LRUCache obj = new LRUCache(capacity);
43 * int param_1 = obj.get(key);
44 * obj.put(key,value);
45 */
好了,这个题目现在已经完成了,然而光这些,在实际开发中缓存往往会被多线程同时访问,我们考虑的就需要更加周密,也更能体现我们的水平。那么线程安全的实现需要一些说明额外的东西呢?
答案就是——加锁。因为对缓存队列和map的操作是可能多个线程同时进行的,所以我们用可重入锁 ReetrantLock 去保护这一过程即可。ReetrantLock 的原理这里就先不赘述。
考虑线程安全的设计实现
- 不论是get还是put方法,对LRU缓存的map和queue进行操作时必须加锁进行,无论操作是否成功,均需要解锁,所以我们用finally关键字包含unlock操作,代码如下。
1 public class LRUCache {
2
3 private final int capacity;
4
5 private ReentrantLock lock = new ReentrantLock();
6
7 private Map<Integer, Integer> map = new ConcurrentHashMap<>();
8 private Deque<Integer> queue = new LinkedList<Integer>();
9
10 public LRUCache(int capacity) {
11 this.capacity = capacity;
12 }
13
14 public int get(int key) {
15 Integer value = map.get(key);
16 if (value != null) {
17 lock.lock();
18 try {
19 this.queue.remove((Integer)key);
20 this.queue.addLast(key);
21 return value;
22 } finally {
23 lock.unlock();
24 }
25 }
26 return -1;
27 }
28
29 public void put(int key, int value) {
30 if (map.get(key) != null) {
31 lock.lock();
32 try {
33 this.queue.remove((Integer)key);
34 this.queue.addLast(key);
35 map.put(key, value);
36 } finally {
37 lock.unlock();
38 }
39 return;
40 }
41
42 lock.lock();
43 try {
44 if (queue.size() >= capacity) {
45 Integer lruKey = this.queue.pollFirst();
46 map.remove(lruKey);
47 }
48 this.queue.addLast(key);
49 map.put(key, value);
50 } finally {
51 lock.unlock();
52 }
53 }
54 }
55
56 /**
57 * Your LRUCache object will be instantiated and called as such:
58 * LRUCache obj = new LRUCache(capacity);
59 * int param_1 = obj.get(key);
60 * obj.put(key,value);
61 */