第三季尚硅谷面试题
尚硅谷大厂面试题第三季周阳主讲
https://www.bilibili.com/video/BV1Hy4y1B78T
文章目录
JUC
可重入锁
- 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),
不会因为之前已经获取过还没释放而阻塞。 - Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
LockSupport
-
3种让线程等待和唤醒的方法
- 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
- 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
注意:前两种方法 线程先要获得并持有锁,必须在锁块(synchronized或lock)中,且必须要先等待后唤醒,线程才能够被唤醒
-
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供**park()和unpark()**方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
- 调用一次unpark就加1变成1
- 调用一次park会消费permit,也就是将1变成o,同时park立即返回
- 如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1
- 每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个
当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效
-
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞
-
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证
而调用两次park却需要消费两个凭证,证不够,不能放行
AbstractQueuedSynchronizer之AQS
-
抽象的队列同步器;是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石, 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
-
锁,面向锁的使用者;同步器,面向锁的实现者
-
有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理;如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node) ,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果
-
初识AQS
- 为实现阻塞锁和相关同步器提供一个框架,依赖一个先进先出的等待队列,依靠单个原子int值表示同步状态,通过占用和释放方法改变状态值
- AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的 FIFO队列来完成资源获取的排队工作将每条要去抢占资源的线程封装成 一个Node节点来实现锁的分配,通过CAS完成对State值的修改
- 有阻塞就需要排队,实现排队必然需要队列 ———— state变量+CLH双端Node队列
- AQS底层是怎么排队的?:是用LockSupport.pork()来进行排队的
-
解读AQS
Lock接口的实现类,基本都是通过【聚合】了一个【队列同步器】的子类完成线程访问控制的
案例
public class AQSDemo { public static void main(String[] args) { ReentrantLock reentrantLock = new ReentrantLock(); //第一个顾客A,可以直接办理业务 new Thread(() -> { reentrantLock.lock(); try { System.out.println("A thread come in"); try { TimeUnit.MINUTES.sleep(20); } catch (Exception e) { e.printStackTrace(); } } finally { reentrantLock.unlock(); } }, "A").start(); //第二个顾客B,等待办理业务 new Thread(() -> { reentrantLock.lock(); try { System.out.println("B thread come in"); } finally { reentrantLock.unlock(); } }, "B").start(); //第三个顾客C,等待办理业务 new Thread(() -> { reentrantLock.lock(); try { System.out.println("C thread come in"); } finally { reentrantLock.unlock(); } }, "C").start(); } }
lock()
reentrantLock.lock() -> sync.lock() -> abstract void lock() -> NonfairSync lock()
- 进入NonfairSync 的 lock实现方法中,第一个线程执行 if 判断,运用CAS判断当前start值是否为0,即是否空闲
- 线程A进入,将start改为1,线程B,C进入后此时状态为被占用,执行 acquire(1);
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
acquire()
acquire() 有三条流程
- 调用tryAcquire() 交由子类的 FairSync 实现
- 调用addWaiter() enq入队操作
- 调用acquireQueued()
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire(arg)
- 此时线程B执行发现 getState() == 1,且当前线程B和正在执行的线程A不是同一线程,直接 false出去
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
addWaiter(Node.EXCLUSIVE)
- 线程B在上一步false取反为true后,准备加入CLH队列,进入acquireQueued 里的 addWaiter
- 因为当前 队列尾节点 pred ==null,所以 进入 enq()
- enq() 可知,如果当前没有任何元素进入队列,就新建一个元素做头节点(傀儡节点、哨兵节点),不存储任何信息,只充当占位符,此时队列头尾指针指向哨兵节点
- 循环之后,此时队列不为空,将线程B存入队列,B节点的前一个指针指向哨兵节点(24),执行交换队列的尾指针指向B节点(25),哨兵节点的下一个指针指向B节点(26)
- 此时线程C也到这里,因为队列尾节点 pred != null,将C节点的前一个指针指向当前尾节点(6),即B节点,执行交换队列的尾指针指向C节点(7),B节点的下一个指针指向C节点(8)
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
- 线程B走到(7)再抢抢假如失败就会进入 shouldParkAfterFailedAcquire(),此时哨兵节点的 waitstatus == 0,交换后,waitstatus == -1
- 如果前驱节点的waitstatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true
- 程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起,此时线程B,C挂起状态
- 当线程A终于完成,将线程B解除挂起状态,因为没有被中断,返回false(54),此时自旋tryAcquire() 再抢抢,发现成功(7)
- 修改队列头节点为B节点,将队列中的B节点置空,将指向哨兵节点的前指针删除,GC删除哨兵节点,让B节点变为新的哨兵节点
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } } private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
unlock()
sync.release(1) -> tryRelease() -> unparkSuccessor() -> unpark() -> parkAndCheckInterrupt()
- 线程A终于完成后,进入unlock(),走到tryRelease(),将state状态设置为0,返回true
- 此时队列不为空,且哨兵节点的waitStatus == -1(8),走到unparkSuccessor()方法中(9),将哨兵节点的waitStatus 改为0
- 此时指针指向B节点(44),此时B节点不为空,且waitStatus == 0,执行 unpark()(52)将挂起的线程B放行
public void unlock() { sync.release(1); } public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
-
AQS考点:
- 看过源码了,那么AQS里面有个变量叫State,它的值有几种?:3种,大于1就是可重入锁,占用就是1,空闲就是0
- 如果AB两个线程进来了以后,请问这个总共有多少个Node节点?:3个,含傀儡节点
Spring
spring的aop顺序
-
Aop常用注解:
- @Before:前置通知: 目标方法之前执行
- @After:后置通知: 目标方法之后执行(始终执行)
- @AfterReturning:返回后通知: 执行方法结束前执行(异常不执行)
- @AfterThrowing:异常通知: 出现异常时候执行
- @Around:环绕通知: 环绕目标方法执行
-
结论
-
Spring4:
-
正常执行:环绕通知前——前置通知——执行方法——环绕通知后——后置通知——返回后通知
-
异常执行:环绕通知前——前置通知——后置通知——异常通知
-
-
Spring5:
-
正常执行:环绕通知前——前置通知——执行方法——返回后通知——后置通知——环绕通知后
-
异常执行:环绕通知前——前置通知——异常通知——后置通知
-
-
spring的循环依赖
-
循环依赖
多个bean之间相互依赖,形成了一个闭环。 比如:A依赖于B、B依赖于c、c依赖于A;通常来说,如果问spring容器内部如何解决循环依赖, 一定是指默认的单例Bean中,属性互相引用的场景;也就是说,Spring的循环依赖,是Spring容器注入时候出现的问题
-
解决循环依赖方法
-
以构造器方式注入依赖:没有办法解决循环依赖, 你想让构造器注入支持循环依赖,是不存在的
-
以set方式注入依赖
-
默认的单例(singleton)的场景是支持循环依赖的,不报错;原型(Prototype)的场景是不支持循环依赖的,报错
只有单例的bean通过三级缓存提前暴露解决循环依赖问题,而非单例的bean每次从容器中获取的都是一个新的对象,都会重新创建,所以非单例的bean没有缓存,不会将其放入三级缓存中;所谓的三级缓存其实就是spring容器内部用来解决循环依赖问题的三个map
-
三级缓存
第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
第三级缓存: Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂getSingleton——doCreateBean——populateBean——addSingletion
- A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
- B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A
然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A - B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)
然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
-
三级缓存Debug
- 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
- 在getSingleton()方法中,从一级缓存中查找,没有,返回null
- doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
- 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
- 进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断。是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中〈即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
- 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
- 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
- 此时beanB依赖于beanA,调用getsingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
- 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
- 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中
-
总结spring是如何解决的循环依赖
- Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化
- 每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个
- 当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完 beanB填充属性时又发现它依赖了beanA又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建
- 既然 beanB创建好了,所以beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
Spring解决循环依赖依靠的是Bean的“中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态……>半成品。实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。
假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程。
Redis
Redis传统五大基本类型的落地应用
小细节:命令不区分大小写,而key是区分大小写的;help @类型名词
官网命令大全:http://www.redis.cn/commands.html
String
常用:set key value;get key
同时设置/获取多个键值对:MSET key value [key value];MGET key [key…]
递增数字:INCR key
增加指定整数:INCRBY key increment
递减数字:DECR key
减少指定整数:DECRBY key decrement
获取字符串长度:STRLEN key
分布式锁:set key value [Ex seconds] [PX milliseconds] [NX|XX]
应用场景:商品编号、订单号采用INCR命令生成、喜欢的文章点赞
hash
Map<String,Map<Object,Object>>
一次设置一个字段值:HSET key field value
一次获取一个字段值:HGET key field
一次设置多个字段值:HMSET key field value [field value…]
一次获取多个字段值:HMGET key field [field…]
获取所有字段值:hgetall key
获取某个key内的全部数量:hlen
删除一个key:hdel
应用场景:购物车,小中厂使用
list
向列表左边添加元素:LPUSH key value [value…]
向列表右边添加元素:RPUSH key value [value…]
查看列表:LRANGE key start stop
获取列表中元素的个数:LLEN key
应用场景:微信文章订阅公众号
set
添加元素:SADD key member [member…]
删除元素:SREM key member [member…]
获取集合中的所有元素:SMEMBERS key
判断元素是否在集合中:SISMEMBER key member
获取集合中的元素个数:SCARD key
从集合中随机弹出一个元素,元素不删除:SRANDMEMBER key [数字]
从集合中随机弹出一个元素,出一个删一个:SPOP key [数字]
集合的差集运算:属于A但不属于B的元素构成的集合 SDIFF key [key…]
集合的交集运算:属于A同时也属于B的元素构成的集合 SINTER key [key…]
集合的并集运算:属于A或者属于B的元素构成的集合 SUNION key [key…]
应用场景:微信抽奖小程序、微信好友共同关注、QQ推荐可能认识的人、朋友圈点赞
zset
向有序集合中加入一个元素和该元素的分数
添加元素:ZADD key score member [score member…]
按元素分数从小到大顺序,返回索引start到stop之间的所有元素:ZRANGE key start stop [WITHSCORES]
获取元素的分数:ZSCORE key member
删除元素:ZREM key member [member…]
获取指定分数范围的元素:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
增加某个元素的分数:ZINCRBY key increment member
获取集合中的元素数量:ZCARD key
获取指定分数范围内的元素个数:COUNT key min max
获取元素排名:从大到小:ZERVRANK key member;从小到大:ZRANK key member
按照排名范围删除元素:ZREMRANGEBYRANK key start stop
应用场景:根据商品销售对商品进行排序显示、抖音热搜
对Redis分布式锁的理解, 删key的时候有什么问题
- synchronized单机版OK,上分布式,分布式部署后,单机锁还是出现超卖现象,需要分布式锁
- nginx分布式微服务 单机锁不行
- 取消单机锁上redis分布式锁setnx
- 只加了锁,没有释放锁, 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
- 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
- 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行的原子性操作
- 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
- lua或者事务
- redis集群环境下,我们自己写的也不OK,直接上RedLock之Redisson落地实现
Redis缓存过期淘汰策略
redis默认内存多少?在哪里查看? 如何设置修改?
- 打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型,注意转换。
- 在64位操作系统下不限制默认内存大小,32位操作系统下最多使用3GB
- 一般通过Redis设置内存为最大物理内存的四分之三,即0.75
- 查看redis内存使用情况:info memory
- 通过命令修改内存大小:config set maxmemory 数值
- 真要打满了会怎么样? 如果Redis内存使用超出了设置的最大值会怎样? 引出Redis缓存过期淘汰策略
过期的删除策略:定期删除、惰性删除、折中
- noeviction:不会驱逐任何key
- allkeys-lru:对所有key使用LRU算法进行删除
- volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
- allkeys-random:对所有key随机删除
- volatile-random:对所有设置了过期时间的key随机删除
- volatile-ttl:删除马上要过期的key
- allkeys-lfu:对所有key使用LFu算法进行删除
- volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除
Redis的LRU算法简介
-
算法来源:https://leetcode-cn.com/problems/lru-cache/
-
设计思想:
1 所谓缓存,必须要有读+写两个操作,按照命中率的思路考虑,写操作+读操作时间复杂度都需要为O(1)
2 特性要求分析
2.1 必须有顺序之分,以区分最近使用的和很久没用到的数据排序。
2.2 写和读操作 一次搞定。
2.3 如果容量(坑位)满了要删除最不长用的数据,每次新访问还要把新的数据插入到队头(按照业务你自己设定左右那一边是队头) -
LRU的算法核心就是哈希链表:本质就是HashMap+DoubleLinkedList 时间复杂度是O(1),哈希表+双向链表的结合体
-
手写LRU
参考LinkedHashMap:
-
依赖JDK
package com.liner.study.lru; import java.util.LinkedHashMap; import java.util.Map; public class LRUCacheDemo<K,V> extends LinkedHashMap<K, V> { private int capacity;//缓存坑位 public LRUCacheDemo(int capacity) { super(capacity,0.75F,false); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return super.size() > capacity; } public static void main(String[] args) { LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3); lruCacheDemo.put(1,"a"); lruCacheDemo.put(2,"b"); lruCacheDemo.put(3,"c"); System.out.println(lruCacheDemo.keySet()); lruCacheDemo.put(4,"d"); System.out.println(lruCacheDemo.keySet()); lruCacheDemo.put(3,"c"); System.out.println(lruCacheDemo.keySet()); lruCacheDemo.put(3,"c"); System.out.println(lruCacheDemo.keySet()); lruCacheDemo.put(3,"c"); System.out.println(lruCacheDemo.keySet()); lruCacheDemo.put(5,"x"); System.out.println(lruCacheDemo.keySet()); } } /** * true * [1, 2, 3] * [2, 3, 4] * [2, 4, 3] * [2, 4, 3] * [2, 4, 3] * [4, 3, 5] * */ /** [1, 2, 3] [2, 3, 4] [2, 3, 4] [2, 3, 4] [2, 3, 4] [3, 4, 5] */
-
不依赖JDK
package com.liner.study.lru; import org.w3c.dom.Node; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; public class LRUCacheDemo{ //map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载体。 //1.构造一个node节点作为数据载体 class Node<K, V> { K key; V value; Node<K,V> prev; Node<K,V> next; public Node(){ this.prev = this.next = null; } public Node(K key, V value) { this.key = key; this.value = value; this.prev = this.next = null; } } //2 构建一个虚拟的双向链表,,里面安放的就是我们的Node class DoubleLinkedList<K, V> { Node<K, V> head; Node<K, V> tail; public DoubleLinkedList(){ head = new Node<>(); tail = new Node<>(); head.next = tail; tail.prev = head; } //3. 添加到头 public void addHead(Node<K,V> node) { node.next = head.next; node.prev = head; head.next.prev = node; head.next = node; } //4.删除节点 public void removeNode(Node<K, V> node) { node.next.prev = node.prev; node.prev.next = node.next; node.prev = null; node.next = null; } //5.获得最后一个节点 public Node getLast() { return tail.prev; } } private int cacheSize; Map<Integer,Node<Integer,Integer>> map; DoubleLinkedList<Integer,Integer> doubleLinkedList; public LRUCacheDemo(int cacheSize) { this.cacheSize = cacheSize;//坑位 map = new HashMap<>();//查找 doubleLinkedList = new DoubleLinkedList<>(); } public int get(int key){ if (!map.containsKey(key)){ return -1; } Node<Integer, Integer> node = map.get(key); doubleLinkedList.removeNode(node); doubleLinkedList.addHead(node); return node.value; } public void put(int key, int value) { if (map.containsKey(key)){ //update Node<Integer, Integer> node = map.get(key); node.value = value; map.put(key, node); doubleLinkedList.removeNode(node); doubleLinkedList.addHead(node); }else { if (map.size() == cacheSize) //坑位满了 { Node<Integer,Integer> lastNode = doubleLinkedList.getLast(); map.remove(lastNode.key); doubleLinkedList.removeNode(lastNode); } //新增一个 Node<Integer, Integer> newNode = new Node<>(key, value); map.put(key,newNode); doubleLinkedList.addHead(newNode); } } public static void main(String[] args) { LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3); lruCacheDemo.put(1,1); lruCacheDemo.put(2,2); lruCacheDemo.put(3,3); System.out.println(lruCacheDemo.map.keySet()); lruCacheDemo.put(4,1); System.out.println(lruCacheDemo.map.keySet()); lruCacheDemo.put(3,1); System.out.println(lruCacheDemo.map.keySet()); lruCacheDemo.put(3,1); System.out.println(lruCacheDemo.map.keySet()); lruCacheDemo.put(3,1); System.out.println(lruCacheDemo.map.keySet()); lruCacheDemo.put(5,1); System.out.println(lruCacheDemo.map.keySet()); } } /** * true * [1, 2, 3] * [2, 3, 4] * [2, 4, 3] * [2, 4, 3] * [2, 4, 3] * [4, 3, 5] * */ /** [1, 2, 3] [2, 3, 4] [2, 3, 4] [2, 3, 4] [2, 3, 4] [3, 4, 5] */
-