LRU cache快取簡單實現

曉乎發表於2020-07-27

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
    }
}

  

相關文章