LRU cache
LRU(最近最少使用)是一種常用的快取淘汰機制。當快取大小容量到達最大分配容量的時候,就會將快取中最近訪問最少的物件刪除掉,以騰出空間給新來的資料。
實現
(1)單執行緒簡單版本
( 來源:力扣(LeetCode)連結:leetcode題目)
題目: 設計和構建一個“最近最少使用”快取,該快取會刪除最近最少使用的專案。快取應該從鍵對映到值(允許你插入和檢索特定鍵對應的值),並在初始化時指定最大容量。當快取被填滿時,它應該刪除最近最少使用的專案。它應該支援以下操作: 獲取資料 get 和 寫入資料 put 。
獲取資料 get(key) - 如果金鑰 (key) 存在於快取中,則獲取金鑰的值(總是正數),否則返回 -1。
寫入資料 put(key, value) - 如果金鑰不存在,則寫入其資料值。當快取容量達到上限時,它應該在寫入新資料之前刪除最近最少使用的資料值,從而為新的資料值留出空間。
思路:LinkedList + HashMap: LinkedList用來儲存key的訪問情況,最近訪問的key將會放置到連結串列的最尾端,如果連結串列大小超過容量,移除連結串列的第一個節點,同時移除該key在hashmap中對應的鍵值對。程式如下:
class LRUCache { private HashMap<Integer, Integer> hashMap = null; private LinkedList<Integer> list = null; private int capacity; public LRUCache(int capacity) { hashMap = new HashMap<>(capacity); list = new LinkedList<Integer>(); this.capacity = capacity; } public int get(int key) { if(hashMap.containsKey(key)){ list.remove((Object)key); list.addLast(key); return hashMap.get(key); } return -1; } public void put(int key, int value) { if(list.contains((Integer)key)){ list.remove((Integer)key); list.addLast((Integer)key); hashMap.put(key, value); return; } if(list.size() == capacity){ Integer v = list.get(0); list.remove(0); hashMap.remove((Object)v); } list.addLast(key); hashMap.put(key, value); } }
(2)多執行緒併發版LRU Cache
與單執行緒思路類似,將HashMap和LinkedList換成支援執行緒安全的容器ConcurrentHashMap和ConcurrentLinkedQueue結構。ConcurrentLinkedQueue是一個基於連結串列,支援先進先出的的佇列結構,處理方法同單執行緒類似,只不過為了保證多執行緒下的安全問題,我們會使用支援讀寫分離鎖的ReadWiterLock來保證執行緒安全。它可以實現:
1.同一時刻,多個執行緒同時讀取共享資源。
2.同一時刻,只允許單個執行緒進行寫操作。
/* * 泛型中萬用字元 * ? 表示不確定的 java 型別 * T (type) 表示具體的一個java型別 * K V (key value) 分別代表java鍵值中的Key Value * E (element) 代表Element */ public class MyLRUCache<K, V> { private final int capacity; private ConcurrentHashMap<K, V> cacheMap; private ConcurrentLinkedQueue<K> keys; ReadWriteLock RWLock = new ReentrantReadWriteLock(); /* * 讀寫鎖 */ private Lock readLock = RWLock.readLock(); private Lock writeLock = RWLock.writeLock(); private ScheduledExecutorService scheduledExecutorService; public MyLRUCache(int capacity) { this.capacity = capacity; cacheMap = new ConcurrentHashMap<>(capacity); keys = new ConcurrentLinkedQueue<>(); scheduledExecutorService = Executors.newScheduledThreadPool(10); } public boolean put(K key, V value, long expireTime){ writeLock.lock(); try { //需要注意containsKey和contains方法方法的區別 if(cacheMap.containsKey(key)){ keys.remove(key); keys.add(key); cacheMap.put(key, value); return true; } if(cacheMap.size() == capacity){ K tmp = keys.poll(); if( key != null){ cacheMap.remove(tmp); } } cacheMap.put(key, value); keys.add(key); if(expireTime > 0){ removeAfterExpireTime(key, expireTime); } return true; }finally { writeLock.unlock(); } } public V get(K key){ readLock.lock(); try { if(cacheMap.containsKey(key)){ keys.remove(key); keys.add(key); return cacheMap.get(key); } return null; }finally { readLock.unlock(); } } public boolean remove(K key){ writeLock.lock(); try { if(cacheMap.containsKey(key)){ cacheMap.remove(key); keys.remove(key); return true; } return false; }finally { writeLock.unlock(); } } private void removeAfterExpireTime(K key, long expireTime){ scheduledExecutorService.schedule(new Runnable() { @Override public void run() { cacheMap.remove(key); keys.remove(key); } }, expireTime, TimeUnit.MILLISECONDS); } public int size(){ return cacheMap.size(); } }
在程式碼中新增了設定鍵值對失效的put方法,通過使用一個定時器執行緒池保證過期鍵值對的及時清理。測試程式碼如下:
public class LRUTest { public static void main(String[] args) throws InterruptedException { /* MyLRUCache<String, Integer> myLruCache = new MyLRUCache(100000); ExecutorService es = Executors.newFixedThreadPool(10); AtomicInteger atomicInteger = new AtomicInteger(1); CountDownLatch latch = new CountDownLatch(10); long starttime = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { es.submit(new Runnable() { @Override public void run() { for (int j = 0; j < 100000; j++) { int v = atomicInteger.getAndIncrement(); myLruCache.put(Thread.currentThread().getName() + "_" + v, v, 200000); } latch.countDown(); } }); } latch.await(); long endtime = System.currentTimeMillis(); es.shutdown(); System.out.println("Cache size:" + myLruCache.size()); //Cache size:1000000 System.out.println("Time cost: " + (endtime - starttime)); */ MyLRUCache<Integer, String> myLruCache = new MyLRUCache<>( 10); myLruCache.put(1, "Java", 1000); myLruCache.put(2, "C++", 2000); myLruCache.put(3, "Java", 3000); System.out.println(myLruCache.size());//3 Thread.sleep(2200); System.out.println(myLruCache.size());//1 } }