尚硅谷大厂面试题第三季周阳主讲整理笔记

第三季尚硅谷面试题

尚硅谷大厂面试题第三季周阳主讲
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()

    1. 进入NonfairSync 的 lock实现方法中,第一个线程执行 if 判断,运用CAS判断当前start值是否为0,即是否空闲
    2. 线程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() 有三条流程

    1. 调用tryAcquire() 交由子类的 FairSync 实现
    2. 调用addWaiter() enq入队操作
    3. 调用acquireQueued()
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
  • tryAcquire(arg)

    1. 此时线程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)

    1. 线程B在上一步false取反为true后,准备加入CLH队列,进入acquireQueued 里的 addWaiter
    2. 因为当前 队列尾节点 pred ==null,所以 进入 enq()
    3. enq() 可知,如果当前没有任何元素进入队列,就新建一个元素做头节点(傀儡节点、哨兵节点),不存储任何信息,只充当占位符,此时队列头尾指针指向哨兵节点
    4. 循环之后,此时队列不为空,将线程B存入队列,B节点的前一个指针指向哨兵节点(24),执行交换队列的尾指针指向B节点(25),哨兵节点的下一个指针指向B节点(26)
    5. 此时线程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)

    1. 线程B走到(7)再抢抢假如失败就会进入 shouldParkAfterFailedAcquire(),此时哨兵节点的 waitstatus == 0,交换后,waitstatus == -1
    2. 如果前驱节点的waitstatus是SIGNAL状态(-1),即shouldParkAfterFailedAcquire方法会返回true
    3. 程序会继续向下执行parkAndCheckInterrupt方法,用于将当前线程挂起,此时线程B,C挂起状态
    4. 当线程A终于完成,将线程B解除挂起状态,因为没有被中断,返回false(54),此时自旋tryAcquire() 再抢抢,发现成功(7)
    5. 修改队列头节点为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()

      1. 线程A终于完成后,进入unlock(),走到tryRelease(),将state状态设置为0,返回true
      2. 此时队列不为空,且哨兵节点的waitStatus == -1(8),走到unparkSuccessor()方法中(9),将哨兵节点的waitStatus 改为0
      3. 此时指针指向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考点:

    1. 看过源码了,那么AQS里面有个变量叫State,它的值有几种?:3种,大于1就是可重入锁,占用就是1,空闲就是0
    2. 如果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容器注入时候出现的问题

  • 解决循环依赖方法

    • 官网解释Core Technologies (spring.io)

    • 以构造器方式注入依赖:没有办法解决循环依赖, 你想让构造器注入支持循环依赖,是不存在的

    • 以set方式注入依赖

    • 默认的单例(singleton)的场景是支持循环依赖的,不报错;原型(Prototype)的场景是不支持循环依赖的,报错

      只有单例的bean通过三级缓存提前暴露解决循环依赖问题,而非单例的bean每次从容器中获取的都是一个新的对象,都会重新创建,所以非单例的bean没有缓存,不会将其放入三级缓存中;所谓的三级缓存其实就是spring容器内部用来解决循环依赖问题的三个map

  • 三级缓存

    第一级缓存〈也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
    第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
    第三级缓存: Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂

    getSingleton——doCreateBean——populateBean——addSingletion

    1. A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
    2. B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A
      然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
    3. B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)
      然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
  • 三级缓存Debug

    1. 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
    2. 在getSingleton()方法中,从一级缓存中查找,没有,返回null
    3. doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
    4. 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean方法
    5. 进入AbstractAutowireCapableBeanFactory#doCreateBean,先反射调用构造器创建出beanA的实例,然后判断。是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中〈即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
    6. 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
    7. 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
    8. 此时beanB依赖于beanA,调用getsingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
    9. 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
    10. 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中
  • 总结spring是如何解决的循环依赖

    1. Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化
    2. 每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个
    3. 当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完 beanB填充属性时又发现它依赖了beanA又是同样的流程,不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建
    4. 既然 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的时候有什么问题

  1. synchronized单机版OK,上分布式,分布式部署后,单机锁还是出现超卖现象,需要分布式锁
  2. nginx分布式微服务 单机锁不行
  3. 取消单机锁上redis分布式锁setnx
  4. 只加了锁,没有释放锁, 出异常的话,可能无法释放锁, 必须要在代码层面finally释放锁
  5. 宕机了,部署了微服务代码层面根本没有走到finally这块,没办法保证解锁,这个key没有被删除,需要有lockKey的过期时间设定
  6. 为redis的分布式锁key,增加过期时间,此外,还必须要setnx+过期时间必须同一行的原子性操作
  7. 必须规定只能自己删除自己的锁,你不能把别人的锁删除了,防止张冠李戴,1删2,2删3
  8. lua或者事务
  9. 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]
       */
      
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
尚硅谷周阳老师的Docker教程的相关学习笔记包括以下内容:首先是Docker的简介,介绍了Docker的基本概念和特点。其次是构建Docker镜像的步骤,包括编写Dockerfile文件、使用docker build命令构建镜像、以及使用docker run命令运行镜像。最后是Dockerfile构建过程的基础知识。 具体来说,Dockerfile是用来定义Docker镜像的构建过程的文件,其中包含了一系列的指令,例如选择基础镜像、安装软件、配置环境等。通过使用docker build命令,我们可以根据Dockerfile构建出一个新的镜像。而使用docker run命令则可以运行该镜像。 以上是尚硅谷周阳老师的Docker教程的相关学习笔记的简要内容。如果需要更详细的信息,建议查阅原教程。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [【学习笔记尚硅谷周阳老师的Docker教程学习笔记](https://blog.csdn.net/zuzhiang/article/details/117339172)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [docker笔记-基础篇](https://blog.csdn.net/xuanxxxxxx/article/details/108511352)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天你学Java了吗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值