划重点,手写一个 LRU 缓存在面试中还是挺常见的!
很多人就会问了:“网上已经有这么多现成的缓存了!为什么面试官还要我们自己实现一个呢?”。
咳咳咳,当然是为了面试需要。哈哈!开个玩笑,我个人觉得更多地是为了学习吧!
今天教大家:
- 实现一个线程安全的 LRU 缓存
- 实现一个线程安全并且带有过期时间的 LRU 缓存
考虑到了线程安全性我们使用了 ConcurrentHashMap 、ConcurrentLinkedQueue 这两个线程安全的集合。另外,还用到 ReadWriteLock(读写锁)。
为了实现带有过期时间的缓存,我们用到了 ScheduledExecutorService来做定时任务执行。
如果有任何不对或者需要完善的地方,请帮忙指出!
1. LRU 缓存介绍
LRU (Least Recently Used,最近最少使用)是一种缓存淘汰策略。 LRU缓存指的是当缓存大小已达到最大分配容量的时候,如果再要去缓存新的对象数据的话,就需要将缓存中最近访问最少的对象删除掉以便给新来的数据腾出空间。
2. ConcurrentLinkedQueue简单介绍
ConcurrentLinkedQueue是一个基于单向链表的无界无锁线程安全的队列,适合在高并发环境下使用,效率比较高。
我们在使用的时候,可以就把它理解为我们经常接触的数据结构——队列,不过是增加了多线程下的安全性保证罢了。和普通队列一样,它也是按照先进先出(FIFO)的规则对接点进行排序。 另外,队列元素中不可以放置null元素。
ConcurrentLinkedQueue 整个继承关系如下图所示:
ConcurrentLinkedQueue中最主要的两个方法是:offer(value)和poll(),分别实现队列的两个重要的操作:入队和出队(offer(value)等价于 add(value))。
我们添加一个元素到队列的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
利用ConcurrentLinkedQueue队列先进先出的特性,每当我们 put/get(缓存被使用)元素的时候,我们就将这个元素存放在队列尾部,这样就能保证队列头部的元素是最近最少使用的。
3. ReadWriteLock简单介绍
ReadWriteLock 是一个接口,位于java.util.concurrent.locks包下,里面只有两个方法分别返回读锁和写锁:
public interface ReadWriteLock {
/**
* 返回读锁
*/
Lock readLock();
/**
* 返回写锁
*/
Lock writeLock();
}
ReentrantReadWriteLock 是ReadWriteLock接口的具体实现类。
读写锁还是比较适合缓存这种读多写少的场景。读写锁可以保证多个线程和同时读取,但是只有一个线程可以写入。但是,有一个问题是当读锁被线程持有的时候,读锁是无法被其它线程申请的,会处于阻塞状态,直至读锁被释放。
另外,同一个线程持有写锁时是可以申请读锁,但是持有读锁的情况下不可以申请写锁。
4. ScheduledExecutorService 简单介绍
ScheduledExecutorService 是一个接口,ScheduledThreadPoolExecutor 是其主要实现类。
ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目用到的比较少,因为有其他方案选择比如quartz。但是,在一些需求比较简单的场景下还是非常有用的!
ScheduledThreadPoolExecutor 使用的任务队列 DelayQueue 封装了一个 PriorityQueue,PriorityQueue 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行,如果执行所需时间相同则先提交的任务将被先执行。
5. 徒手撸一个线程安全的 LRU 缓存
5.1 实现方法
ConcurrentHashMap + Concur