劃重點,手寫一個 LRU 緩存在面試中仍是挺常見的!java
不少人就會問了:「網上已經有這麼多現成的緩存了!爲何面試官還要咱們本身實現一個呢?」。面試
咳咳咳,固然是爲了面試須要。哈哈!開個玩笑,我我的以爲更多地是爲了學習吧!spring
今天教你們:設計模式
考慮到了線程安全性咱們使用了 ConcurrentHashMap 、ConcurrentLinkedQueue 這兩個線程安全的集合。另外,還用到 ReadWriteLock(讀寫鎖)。緩存
爲了實現帶有過時時間的緩存,咱們用到了 ScheduledExecutorService來作定時任務執行。安全
若是有任何不對或者須要完善的地方,請幫忙指出!網絡
LRU (Least Recently Used,最近最少使用)是一種緩存淘汰策略。
LRU緩存指的是當緩存大小已達到最大分配容量的時候,若是再要去緩存新的對象數據的話,就須要將緩存中最近訪問最少的對象刪除掉以便給新來的數據騰出空間。數據結構
ConcurrentLinkedQueue是一個基於單向鏈表的無界無鎖線程安全的隊列,適合在高併發環境下使用,效率比較高。 多線程
咱們在使用的時候,能夠就把它理解爲咱們常常接觸的數據結構——隊列,不過是增長了多線程下的安全性保證罷了。和普通隊列同樣,它也是按照先進先出(FIFO)的規則對接點進行排序。 另外,隊列元素中不能夠放置null元素。併發
ConcurrentLinkedQueue 整個繼承關係以下圖所示:
ConcurrentLinkedQueue中最主要的兩個方法是:offer(value)和poll(),分別實現隊列的兩個重要的操做:入隊和出隊(offer(value)等價於 add(value))。
咱們添加一個元素到隊列的時候,它會添加到隊列的尾部,當咱們獲取一個元素時,它會返回隊列頭部的元素。
利用ConcurrentLinkedQueue隊列先進先出的特性,每當咱們 put/get(緩存被使用)元素的時候,咱們就將這個元素存放在隊列尾部,這樣就能保證隊列頭部的元素是最近最少使用的。
ReadWriteLock 是一個接口,位於java.util.concurrent.locks包下,裏面只有兩個方法分別返回讀鎖和寫鎖:
public interface ReadWriteLock { /** * 返回讀鎖 */ Lock readLock(); /** * 返回寫鎖 */ Lock writeLock(); }
ReentrantReadWriteLock 是ReadWriteLock接口的具體實現類。
讀寫鎖仍是比較適合緩存這種讀多寫少的場景。讀寫鎖能夠保證多個線程和同時讀取,可是隻有一個線程能夠寫入。可是,有一個問題是當讀鎖被線程持有的時候,讀鎖是沒法被其它線程申請的,會處於阻塞狀態,直至讀鎖被釋放。
另外,同一個線程持有寫鎖時是能夠申請讀鎖,可是持有讀鎖的狀況下不能夠申請寫鎖。
ScheduledExecutorService 是一個接口,ScheduledThreadPoolExecutor 是其主要實現類。
ScheduledThreadPoolExecutor 主要用來在給定的延遲後運行任務,或者按期執行任務。 這個在實際項目用到的比較少,由於有其餘方案選擇好比quartz。可是,在一些需求比較簡單的場景下仍是很是有用的!
ScheduledThreadPoolExecutor 使用的任務隊列 DelayQueue 封裝了一個 PriorityQueue,PriorityQueue 會對隊列中的任務進行排序,執行所需時間短的放在前面先被執行,若是執行所需時間相同則先提交的任務將被先執行。
5.1 實現方法
ConcurrentHashMap + ConcurrentLinkedQueue +ReadWriteLock
5.2 原理
ConcurrentHashMap 是線程安全的Map,咱們能夠利用它緩存 key,value形式的數據。
ConcurrentLinkedQueue是一個線程安全的基於鏈表的隊列(先進先出),咱們能夠用它來維護 key 。
每當咱們put/get(緩存被使用)元素的時候,咱們就將這個元素對應的 key 存放在隊列尾部,這樣就能保證隊列頭部的元素是最近最少使用的。
當咱們的緩存容量不夠的時候,咱們直接移除隊列頭部對應的key以及這個key對應的緩存便可!
另外,咱們用到了ReadWriteLock(讀寫鎖)來保證線程安全。
5.3 put方法具體流程分析
爲了方便你們理解,我將代碼中比較重要的 put(key,value)方法的原理圖畫了出來,以下圖所示:
5.4 源碼
/** * @author shuang.kou * <p> * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock實現線程安全的 LRU 緩存 * 這裏只是爲了學習使用,本地緩存推薦使用 Guava 自帶的,使用 Spring 的話,推薦使用Spring Cache */ public class MyLruCache<K, V> { /** * 緩存的最大容量 */ private final int maxCapacity; private ConcurrentHashMap<K, V> cacheMap; private ConcurrentLinkedQueue<K> keys; /** * 讀寫鎖 */ private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private Lock writeLock = readWriteLock.writeLock(); private Lock readLock = readWriteLock.readLock(); public MyLruCache(int maxCapacity) { if (maxCapacity < 0) { throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); } this.maxCapacity = maxCapacity; cacheMap = new ConcurrentHashMap<>(maxCapacity); keys = new ConcurrentLinkedQueue<>(); } public V put(K key, V value) { // 加寫鎖 writeLock.lock(); try { //1.key是否存在於當前緩存 if (cacheMap.containsKey(key)) { moveToTailOfQueue(key); cacheMap.put(key, value); return value; } //2.是否超出緩存容量,超出的話就移除隊列頭部的元素以及其對應的緩存 if (cacheMap.size() == maxCapacity) { System.out.println("maxCapacity of cache reached"); removeOldestKey(); } //3.key不存在於當前緩存。將key添加到隊列的尾部而且緩存key及其對應的元素 keys.add(key); cacheMap.put(key, value); return value; } finally { writeLock.unlock(); } } public V get(K key) { //加讀鎖 readLock.lock(); try { //key是否存在於當前緩存 if (cacheMap.containsKey(key)) { // 存在的話就將key移動到隊列的尾部 moveToTailOfQueue(key); return cacheMap.get(key); } //不存在於當前緩存中就返回Null return null; } finally { readLock.unlock(); } } public V remove(K key) { writeLock.lock(); try { //key是否存在於當前緩存 if (cacheMap.containsKey(key)) { // 存在移除隊列和Map中對應的Key keys.remove(key); return cacheMap.remove(key); } //不存在於當前緩存中就返回Null return null; } finally { writeLock.unlock(); } } /** * 將元素添加到隊列的尾部(put/get的時候執行) */ private void moveToTailOfQueue(K key) { keys.remove(key); keys.add(key); } /** * 移除隊列頭部的元素以及其對應的緩存 (緩存容量已滿的時候執行) */ private void removeOldestKey() { K oldestKey = keys.poll(); if (oldestKey != null) { cacheMap.remove(oldestKey); } } public int size() { return cacheMap.size(); } }
非併發環境測試:
MyLruCache<Integer, String> myLruCache = new MyLruCache<>(3); myLruCache.put(1, "Java"); System.out.println(myLruCache.get(1));// Java myLruCache.remove(1); System.out.println(myLruCache.get(1));// null myLruCache.put(2, "C++"); myLruCache.put(3, "Python"); System.out.println(myLruCache.get(2));//C++ myLruCache.put(4, "C"); myLruCache.put(5, "PHP"); System.out.println(myLruCache.get(2));// C++
併發環境測試:
咱們初始化了一個固定容量爲 10 的線程池和count爲10的CountDownLatch。咱們將1000000次操做分10次添加到線程池,而後咱們等待線程池執行完成這10次操做。
int threadNum = 10; int batchSize = 100000; //init cache MyLruCache<String, Integer> myLruCache = new MyLruCache<>(batchSize * 10); //init thread pool with 10 threads ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum); //init CountDownLatch with 10 count CountDownLatch latch = new CountDownLatch(threadNum); AtomicInteger atomicInteger = new AtomicInteger(0); long startTime = System.currentTimeMillis(); for (int t = 0; t < threadNum; t++) { fixedThreadPool.submit(() -> { for (int i = 0; i < batchSize; i++) { int value = atomicInteger.incrementAndGet(); myLruCache.put("id" + value, value); } latch.countDown(); }); } //wait for 10 threads to complete the task latch.await(); fixedThreadPool.shutdown(); System.out.println("Cache size:" + myLruCache.size());//Cache size:1000000 long endTime = System.currentTimeMillis(); long duration = endTime - startTime; System.out.println(String.format("Time cost:%dms", duration));//Time cost:511ms
實際上就是在咱們上面時間的LRU緩存的基礎上加上一個定時任務去刪除緩存,單純利用 JDK 提供的類,咱們實現定時任務的方式有不少種:
最終咱們選擇了 ScheduledExecutorService,主要緣由是它易用(基於DelayQueue作了不少封裝)而且基本能知足咱們的大部分需求。
咱們在咱們上面實現的線程安全的 LRU 緩存基礎上,簡單稍做修改便可!
咱們增長了一個方法:
private void removeAfterExpireTime(K key, long expireTime) { scheduledExecutorService.schedule(() -> { //過時後清除該鍵值對 cacheMap.remove(key); keys.remove(key); }, expireTime, TimeUnit.MILLISECONDS); }
咱們put元素的時候,若是經過這個方法就能直接設置過時時間。
完整源碼以下:
/** * @author shuang.kou * <p> * 使用 ConcurrentHashMap+ConcurrentLinkedQueue+ReadWriteLock+ScheduledExecutorService實現線程安全的 LRU 緩存 * 這裏只是爲了學習使用,本地緩存推薦使用 Guava 自帶的,使用 Spring 的話,推薦使用Spring Cache */ public class MyLruCacheWithExpireTime<K, V> { /** * 緩存的最大容量 */ private final int maxCapacity; private ConcurrentHashMap<K, V> cacheMap; private ConcurrentLinkedQueue<K> keys; /** * 讀寫鎖 */ private ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private Lock writeLock = readWriteLock.writeLock(); private Lock readLock = readWriteLock.readLock(); private ScheduledExecutorService scheduledExecutorService; public MyLruCacheWithExpireTime(int maxCapacity) { if (maxCapacity < 0) { throw new IllegalArgumentException("Illegal max capacity: " + maxCapacity); } this.maxCapacity = maxCapacity; cacheMap = new ConcurrentHashMap<>(maxCapacity); keys = new ConcurrentLinkedQueue<>(); scheduledExecutorService = Executors.newScheduledThreadPool(3); } public V put(K key, V value, long expireTime) { // 加寫鎖 writeLock.lock(); try { //1.key是否存在於當前緩存 if (cacheMap.containsKey(key)) { moveToTailOfQueue(key); cacheMap.put(key, value); return value; } //2.是否超出緩存容量,超出的話就移除隊列頭部的元素以及其對應的緩存 if (cacheMap.size() == maxCapacity) { System.out.println("maxCapacity of cache reached"); removeOldestKey(); } //3.key不存在於當前緩存。將key添加到隊列的尾部而且緩存key及其對應的元素 keys.add(key); cacheMap.put(key, value); if (expireTime > 0) { removeAfterExpireTime(key, expireTime); } return value; } finally { writeLock.unlock(); } } public V get(K key) { //加讀鎖 readLock.lock(); try { //key是否存在於當前緩存 if (cacheMap.containsKey(key)) { // 存在的話就將key移動到隊列的尾部 moveToTailOfQueue(key); return cacheMap.get(key); } //不存在於當前緩存中就返回Null return null; } finally { readLock.unlock(); } } public V remove(K key) { writeLock.lock(); try { //key是否存在於當前緩存 if (cacheMap.containsKey(key)) { // 存在移除隊列和Map中對應的Key keys.remove(key); return cacheMap.remove(key); } //不存在於當前緩存中就返回Null return null; } finally { writeLock.unlock(); } } /** * 將元素添加到隊列的尾部(put/get的時候執行) */ private void moveToTailOfQueue(K key) { keys.remove(key); keys.add(key); } /** * 移除隊列頭部的元素以及其對應的緩存 (緩存容量已滿的時候執行) */ private void removeOldestKey() { K oldestKey = keys.poll(); if (oldestKey != null) { cacheMap.remove(oldestKey); } } private void removeAfterExpireTime(K key, long expireTime) { scheduledExecutorService.schedule(() -> { //過時後清除該鍵值對 cacheMap.remove(key); keys.remove(key); }, expireTime, TimeUnit.MILLISECONDS); } public int size() { return cacheMap.size(); } }
測試效果:
MyLruCacheWithExpireTime<Integer,String> myLruCache = new MyLruCacheWithExpireTime<>(3); myLruCache.put(1,"Java",3000); myLruCache.put(2,"C++",3000); myLruCache.put(3,"Python",1500); System.out.println(myLruCache.size());//3 Thread.sleep(2000); System.out.println(myLruCache.size());//2
文源網絡,僅供學習之用,若有侵權,聯繫刪除。我將面試題和答案都整理成了PDF文檔,還有一套學習資料,涵蓋Java虛擬機、spring框架、Java線程、數據結構、設計模式等等,但不只限於此。
關注公衆號【java圈子】獲取資料,還有優質文章每日送達。