preface
LRU is the abbreviation for Least Recently Used.
The elimination strategy is commonly used for cache implementation. Since cache memory is very valuable, data needs to be eliminated according to some rules to ensure that memory is not full.
Redis, for example, has the following strategies:
strategy | describe |
---|---|
volatile-lru | Select the least recently used data from the data set with expiration time |
volatile-ttl | Select the data to be obsolete from the set with expiration time |
volatile-random | Select any data from a data set with an expiration time |
allkeys-lru | Select the least recently used data from all data sets for elimination |
allkeys-random | Randomly select data from all data sets for elimination |
no-envicition | Deporting data is prohibited |
From: github.com/CyC2018/Int…
Implement a
I have also had contact with an interview question before, and the requirements are as follows:
- Implement an LRU cache. When the cache reaches N, the least recently used data needs to be eliminated.
- Data that has not been accessed within N hours also needs to be eliminated.
Here is my implementation:
public class LRUAbstractMap extends java.util.AbstractMap {
private final static Logger LOGGER = LoggerFactory.getLogger(LRUAbstractMap.class);
/** * Check whether threads are overdue */
private ExecutorService checkTimePool ;
/** * map maximum size */
private final static int MAX_SIZE = 1024 ;
private final static ArrayBlockingQueue<Node> QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ;
/** * Default size */
private final static int DEFAULT_ARRAY_SIZE =1024 ;
/** * Array length */
private int arraySize ;
/** * array */
private Object[] arrays ;
/** * Determine whether to stop flag */
private volatile boolean flag = true ;
/** * Timeout duration */
private final static Long EXPIRE_TIME = 60 * 60 * 1000L ;
/** * The size of the entire Map */
private volatile AtomicInteger size ;
public LRUAbstractMap(a) {
arraySize = DEFAULT_ARRAY_SIZE;
arrays = new Object[arraySize] ;
// Start a thread to check whether the value first queued is expired
executeCheckTime();
}
/** * starts a thread to check if the first value to be queued is expired and set to daemon thread */
private void executeCheckTime(a) {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("check-thread-%d")
.setDaemon(true)
.build();
checkTimePool = new ThreadPoolExecutor(1.1.0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());
checkTimePool.execute(new CheckTimeThread()) ;
}
@Override
public Set<Entry> entrySet(a) {
return super.keySet();
}
@Override
public Object put(Object key, Object value) {
int hash = hash(key);
int index = hash % arraySize ;
Node currentNode = (Node) arrays[index] ;
if (currentNode == null){
arrays[index] = new Node(null.null, key, value);
// Write to queue
QUEUE.offer((Node) arrays[index]) ;
sizeUp();
}else {
Node cNode = currentNode ;
Node nNode = cNode ;
// If it exists, override it
if (nNode.key == key){
cNode.val = value ;
}
while(nNode.next ! =null) {// The presence of the key overrides the simple judgment
if (nNode.key == key){
nNode.val = value ;
break ;
}else {
// Add a list if it does not exist
sizeUp();
Node node = new Node(nNode,null,key,value) ;
// Write to queueQUEUE.offer(currentNode) ; cNode.next = node ; } nNode = nNode.next ; }}return null ;
}
@Override
public Object get(Object key) {
int hash = hash(key) ;
int index = hash % arraySize ;
Node currentNode = (Node) arrays[index] ;
if (currentNode == null) {return null ;
}
if (currentNode.next == null) {// Update time
currentNode.setUpdateTime(System.currentTimeMillis());
// There is no conflict
return currentNode ;
}
Node nNode = currentNode ;
while(nNode.next ! =null) {if (nNode.key == key){
// Update time
currentNode.setUpdateTime(System.currentTimeMillis());
return nNode ;
}
nNode = nNode.next ;
}
return super.get(key);
}
@Override
public Object remove(Object key) {
int hash = hash(key) ;
int index = hash % arraySize ;
Node currentNode = (Node) arrays[index] ;
if (currentNode == null) {return null ;
}
if (currentNode.key == key){
sizeDown();
arrays[index] = null ;
// Remove the queue
QUEUE.poll();
return currentNode ;
}
Node nNode = currentNode ;
while(nNode.next ! =null) {if (nNode.key == key){
sizeDown();
// Next points to the next node of the current node
nNode.pre.next = nNode.next ;
nNode = null ;
// Remove the queue
QUEUE.poll();
return nNode;
}
nNode = nNode.next ;
}
return super.remove(key);
}
/** * add size */
private void sizeUp(a){
// The value of put is thought to contain data
flag = true ;
if (size == null){
size = new AtomicInteger() ;
}
int size = this.size.incrementAndGet();
if (size >= MAX_SIZE) {
// Find the data in the queue header
Node node = QUEUE.poll() ;
if (node == null) {throw new RuntimeException("data error"); }// Remove the keyObject key = node.key ; remove(key) ; lruCallback() ; }}/** * The number is reduced */
private void sizeDown(a){
if (QUEUE.size() == 0){
flag = false ;
}
this.size.decrementAndGet() ;
}
@Override
public int size(a) {
return size.get() ;
}
/**
* 链表
*/
private class Node{
private Node next ;
private Node pre ;
private Object key ;
private Object val ;
private Long updateTime ;
public Node(Node pre,Node next, Object key, Object val) {
this.pre = pre ;
this.next = next;
this.key = key;
this.val = val;
this.updateTime = System.currentTimeMillis() ;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
public Long getUpdateTime(a) {
return updateTime;
}
@Override
public String toString(a) {
return "Node{" +
"key=" + key +
", val=" + val +
'} '; }}/** * copy hash implementation of HashMap *@param key
* @return* /
public int hash(Object key) {
int h;
return (key == null)?0 : (h = key.hashCode()) ^ (h >>> 16);
}
private void lruCallback(a){
LOGGER.debug("lruCallback");
}
private class CheckTimeThread implements Runnable{
@Override
public void run(a) {
while (flag){
try {
Node node = QUEUE.poll();
if (node == null) {continue ;
}
Long updateTime = node.getUpdateTime() ;
if((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){ remove(node.key) ; }}catch (Exception e) {
LOGGER.error("InterruptedException");
}
}
}
}
}
Copy the code
Interested friends can directly from:
Github.com/crossoverJi…
Download the code to run locally.
The code looks more, in fact, the implementation of the idea is relatively simple:
- It uses the same way of saving data as HashMap, but implements a simple version manually.
- Internally, a queue is used to store data for each write.
- Check whether the cache is larger than the threshold N. If yes, delete the data in the queue header according to the FIFO feature of the queue. Because the data in the queue header must be put in first.
- A daemon thread is also started to determine whether the data put in first is expired (because even if it is, the data put in first is most likely to meet the expiration condition).
- Setting it as a daemon thread is a better indication of its purpose (in the worst case, if it is a user thread, it may end up causing the program to fail because the thread is always running, which is not the case with the daemon thread).
The above code satisfies the general function, but there is a fatal problem.
If the least recently used data is not satisfied, the deleted data is the first data to be added.
However, the PUT Get process is a simple implementation of HashMap, and you can understand HashMap a little better.
Realize the
So how to implement a full LRU cache, this time regardless of expiration time.
In fact, there are some ideas from the previous implementation:
- To keep track of the least recent use, at least one ordered set is required to guarantee the order of the writes.
- The ability to update the order of data after it has been used.
Based on these two points, it’s easy to think of a common data structure: linked lists.
- Puts data into the linked list header each time it is written.
- Move data to a header when using data.
- Remove the tail of the list when the number of caches exceeds the threshold.
Hence the following implementation:
public class LRUMap<K.V> {
private final Map<K, V> cacheMap = new HashMap<>();
/** * Maximum cache size */
private int cacheSize;
/** * Node size */
private int nodeCount;
/** ** header */
private Node<K, V> header;
/** * end */
private Node<K, V> tailer;
public LRUMap(int cacheSize) {
this.cacheSize = cacheSize;
// The next node of the header is null
header = new Node<>();
header.next = null;
// The last node of the end node is null
tailer = new Node<>();
tailer.tail = null;
// The top node of the bidirectional list head points to the tail node
header.tail = tailer;
// The lower node of the tail points to the head node
tailer.next = header;
}
public void put(K key, V value) {
cacheMap.put(key, value);
// Add nodes to the bidirectional list
addNode(key, value);
}
public V get(K key){
Node<K, V> node = getNode(key);
// Move the header
moveToHead(node) ;
return cacheMap.get(key);
}
private void moveToHead(Node<K,V> node){
// If it is the last node
if (node.tail == null){
node.next.tail = null ;
tailer = node.next ;
nodeCount -- ;
}
// If it is already the head node, it is not processed
if (node.next == null) {return ;
}
// If it is an intermediate node
if(node.tail ! =null&& node.next ! =null) {// Its previous node points to its next node and deletes the current node
node.tail.next = node.next ;
nodeCount -- ;
}
// Finally add the current node to the header
// Note that a new object is required, otherwise the original Node still has the following reference, causing memory overflow.
node = new Node<>(node.getKey(),node.getValue()) ;
addHead(node) ;
}
/** * List query efficiency is low *@param key
* @return* /
private Node<K,V> getNode(K key){
Node<K,V> node = tailer ;
while(node ! =null) {if (node.getKey().equals(key)){
return node ;
}
node = node.next ;
}
return null ;
}
/** * writes the header *@param key
* @param value
*/
private void addNode(K key, V value) {
Node<K, V> node = new Node<>(key, value);
// Delete the last one
if (cacheSize == nodeCount) {
// Delete the end node
delTail();
}
// Write the header
addHead(node);
}
/** * add header **@param node
*/
private void addHead(Node<K, V> node) {
// Write the header
header.next = node;
node.tail = header;
header = node;
nodeCount++;
// If more than two data are written, the initialized header and tail are deleted
if (nodeCount == 2) {
tailer.next.next.tail = null; tailer = tailer.next.next; }}private void delTail(a) {
// Remove the endnode from the cache
cacheMap.remove(tailer.getKey());
// Delete the end node
tailer.next.tail = null;
tailer = tailer.next;
nodeCount--;
}
private class Node<K.V> {
private K key;
private V value;
Node<K, V> tail;
Node<K, V> next;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public Node(a) {}public K getKey(a) {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue(a) {
return value;
}
public void setValue(V value) {
this.value = value; }}@Override
public String toString(a) {
StringBuilder sb = new StringBuilder() ;
Node<K,V> node = tailer ;
while(node ! =null){
sb.append(node.getKey()).append(":")
.append(node.getValue())
.append("-- >"); node = node.next ; }returnsb.toString(); }}Copy the code
Source: github.com/crossoverJi…
In practice, when writing:
@Test
public void put(a) throws Exception {
LRUMap<String,Integer> lruMap = new LRUMap(3); lruMap.put("1".1); lruMap.put("2".2); lruMap.put("3".3); System.out.println(lruMap.toString()); lruMap.put("4".4); System.out.println(lruMap.toString()); lruMap.put("5".5); System.out.println(lruMap.toString()); }/ / output:
1:1-->2:2-->3:3-->
2:2-->3:3-->4:4-->
3:3-->4:4-->5:5-->
Copy the code
When using:
@Test
public void get(a) throws Exception {
LRUMap<String,Integer> lruMap = new LRUMap(3); lruMap.put("1".1); lruMap.put("2".2); lruMap.put("3".3); System.out.println(lruMap.toString()); System.out.println("= = = = = = = = = = = = = =");
Integer integer = lruMap.get("1");
System.out.println(integer);
System.out.println("= = = = = = = = = = = = = =");
System.out.println(lruMap.toString());
}
/ / output
1:1-->2:2-->3:3-- > = = = = = = = = = = = = = =1= = = = = = = = = = = = = =2:2-->3:3-->1:1-->
Copy the code
To achieve the same idea as mentioned above, let’s say the key points:
- The data is stored directly using the HashMap.
- Internally, a bidirectional linked list is used to store data, so there is a header and a tailer.
- Each time you write a header or delete a tail, you rely on the header tailer. If you feel confused, you are advised to implement a linked list to familiarize yourself with it, or use the following object diagram to understand it.
- When using data to move to the head of a list, the first step is to find the node in the bidirectional list. This is where the problem with linked lists comes in. Search efficiency is very low, the worst need
O(N)
. It then moves depending on the current node. - The initialized header and tail nodes need to be deleted when the size of the linked list is determined to be equal to 2. This is because initialization generates two two-way nodes with no data to form a data structure. When the real data comes in, it needs to be deleted to facilitate subsequent operations (this can be further optimized).
- All of the above operations are thread-unsafe and require user control.
Here is the object diagram:
Initialization time
When writing data
LRUMap<String,Integer> lruMap = new LRUMap(3); lruMap.put("1".1);Copy the code
lruMap.put("2".2);Copy the code
lruMap.put("3".3);Copy the code
lruMap.put("4".4);Copy the code
When obtaining data
The data is the same as above:
Integer integer = lruMap.get("2");
Copy the code
The above pictures should give you a good idea of how the data is stored.
Implement three
In fact, if you are familiar with Java collections, you will notice that the structure above is very similar to LinkedHashMap.
For those not familiar with this, take a look at the underlying LinkedHashMap analysis.
So we can use it to achieve:
public class LRULinkedMap<K.V> {
/** * Maximum cache size */
private int cacheSize;
private LinkedHashMap<K,V> cacheMap ;
public LRULinkedMap(int cacheSize) {
this.cacheSize = cacheSize;
cacheMap = new LinkedHashMap(16.0.75 F.true) {@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
if (cacheSize + 1 == cacheMap.size()){
return true ;
}else {
return false; }}}; }public void put(K key,V value){
cacheMap.put(key,value) ;
}
public V get(K key){
return cacheMap.get(key) ;
}
public Collection<Map.Entry<K, V>> getAll() {
return newArrayList<Map.Entry<K, V>>(cacheMap.entrySet()); }}Copy the code
Source: github.com/crossoverJi…
This time it’s a little more succinct, just a few lines of code (the specific logical LinkedHashMap has already been implemented for us)
Actual effect:
@Test
public void put(a) throws Exception {
LRULinkedMap<String,Integer> map = new LRULinkedMap(3); map.put("1".1);
map.put("2".2);
map.put("3".3);
for (Map.Entry<String, Integer> e : map.getAll()){
System.out.print(e.getKey() + ":" + e.getValue() + "\t");
}
System.out.println("");
map.put("4".4);
for (Map.Entry<String, Integer> e : map.getAll()){
System.out.print(e.getKey() + ":" + e.getValue() + "\t"); }}/ / output
1 : 1 2 : 2 3 : 3
2 : 2 3 : 3 4 : 4
Copy the code
When using:
@Test
public void get(a) throws Exception {
LRULinkedMap<String,Integer> map = new LRULinkedMap(4); map.put("1".1);
map.put("2".2);
map.put("3".3);
map.put("4".4);
for (Map.Entry<String, Integer> e : map.getAll()){
System.out.print(e.getKey() + ":" + e.getValue() + "\t");
}
System.out.println("");
map.get("1");for (Map.Entry<String, Integer> e : map.getAll()){
System.out.print(e.getKey() + ":" + e.getValue() + "\t"); }}}/ / output
1 : 1 2 : 2 3 : 3 4 : 4
2 : 2 3 : 3 4 : 4 1 : 1
Copy the code
LinkedHashMap also maintains a bidirectional queue and is initialized with a buffer size threshold. During initialization, you need to define whether to delete recently infrequently used data. If yes, data is managed according to implementation 2.
The main code overrides the removeEldestEntry method of LinkedHashMap:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
Copy the code
By default, it returns false, that is, it does not care if the threshold is exceeded.
So we custom return true above the threshold so that LinkedHashMap will remove the least recently used data for us.
conclusion
That’s the implementation of the LRU cache, at least in everyday use.
Guava’s implementation is also widely used in the industry, and it supports multiple expiration policies.
extra
Recently in the summary of some Java related knowledge points, interested friends can maintain together.
Address: github.com/crossoverJi…