划重点,手写一个 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圈子】获取资料,还有优质文章每日送达。