LRU缓存设计

 

  LRU缓存设计是一个能够考察许多知识点以及实际编程能力的题目,因为我们在实际工作中是很有可能会去自己写一个LRU算法的简单缓存。本题是LeetCode的第 146 题。LRU——即 Least Recently Used,淘汰最近最少使用的元素的算法。考察的主要内容包括:

  • LRU算法的原理与思想
  • 具有实际开发意义的编程题
  • 线程安全的设计

 

设计思路

  首先考虑简单的设计实现。一个LRU的缓存是基于队列实现的,Java中可以基于LinkedList或者Deque队列实现。需要实现两个基本操作get/put

  1. 数据结构上,缓存需要指定容量,不能随意扩展。
  2. 用一个线程安全的ConcurrentHashMap存储数据的键值对。
  3. 用一个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  */

 

posted @ 2020-06-27 17:24  XiaoH在博客园  阅读(627)  评论(0编辑  收藏  举报