LRU的实现
LRU(Least Recently Used)是一种常见的页面置换算法,在计算中,所有的文件操作都要放在内存中进行,然而计算机内存大小是固定的,所以不可能把所有的文件都加载到内存,因此需要制定一种策略对加入到内存中的文件进项选择。
LRU的设计原理:当数据在最近一段时间经常被访问,那么它在以后也会经常被访问。这就意味着,如果经常访问的数据,需要其能够快速命中,而不常访问的数据,在容量超出限制内,要将其淘汰。当数据按照如下顺序进行访问时,LRU的工作原理如下:
为了实现既能够让其搜索快,又能够快速进行增删操作,这里可以选择 链表+hash表,hash表的搜索可以达到0(1)时间复杂度。hash表选用HashMap;链表,Node一个双向链表的实现。通过HashMap中key存储Node的key,value存储Node来建立Map对Node的映射关系。将HashMap看作是一张检索表,可以快速检索到需要定位的Node。大致思路:
- 构建双向链表节点ListNode,应包含key,value,prev,next这几个基本属性
- 对于Cache对象来说,我们需要规定缓存的容量,所以在初始化时,设置容量大小,然后实例化双向链表的head,tail,并让head.next->tail tail.prev->head,这样我们的双向链表构建完成
- 对于get操作,首先查阅hashmap,如果存在的话,直接将Node从当前位置移除,然后插入到链表的首部,在链表中实现删除直接让node的前驱节点指向后继节点。如果不存在,那么直接返回Null
- 对于put操作
class LRUCache {
class Node {
int key, value;
Node next, pre;
public Node(int key, int value) {
this.key = key;
this.value = value;
next = null;
pre = null;
}
}
class NodeList {
int size;
Node head, tail;
public NodeList() {
size = 0;
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.pre = head;
}
public void addFirst(Node node) {
size ++;
head.next.pre = node;
node.next = head.next;
node.pre = head;
head.next = node;
}
public void remove(Node node) {
node.pre.next = node.next;
node.next.pre = node.pre;
size --;
}
public Node removeLast() {
if (head.next == tail) {
return null;
}
Node node = tail.pre;
remove(node);
return node;
}
public int size() {
return size;
}
}
int capacity;
Map<Integer, Node> map;
NodeList list;
public LRUCache(int capacity) {
this.capacity = capacity;
list = new NodeList();
map = new HashMap<>(capacity);
}
public int get(int key) {
if (!map.containsKey(key)) return - 1;
Node node = map.get(key);
put(key, node.value);
return node.value;
}
public void put(int key, int value) {
Node node = new Node(key, value);
if (map.containsKey(key)) {
Node temp = map.get(key);
map.remove(key);
list.remove(temp);
list.addFirst(node);
map.put(key, node);
} else {
if (list.size() == capacity) {
map.remove(list.removeLast().key);
}
list.addFirst(node);
map.put(key, node);
}
}
}
高吞吐、线程安全的LRU缓存
线程安全
线程安全的思路可以通过ConcurrentHashMap上去考虑,在1.8版本之后虽然取消了段的概念,但是在实现上依然采用的是减小锁粒度思想来实现高并发,这个粒度变得更小了,为每一个桶分配了一个锁。
高效LRU
HashMap部分确实可以采用ConcurrentHashMap的实现去达到线程安全,但是对于维持LRU的双向链表来说,对该链表上的操作性能还是比较低下的,虽然对不同段的节点进行操作可以使用不同的锁,可以实现更高的并发量,但是对于维护LRU还是需要对head和tail进行加锁,这样还是会导致线程之间的串行执行。
可以借鉴减小锁粒度的思想来实现LRU,分段实现锁分离+每个段内维护一份退化链表,即在每一个段中各自维护相应的LRU链表,这样就可以在不同的段真正实现隔离,实现高并发。
【实现策略】:
- 锁分离机制。内部分成了多个segement,每个segement是独立加锁,相互不干扰
- 每个segement内部维护一个双向链表,每次添加,就把节点移动到退化链表头部
- 每次put操作,通过hash,散到每个segement中,判断segment的容量是否到达阈值。如果到达阈值,则删除退化链表中最末尾的节点