最近面试问题总结(持续更新)

本篇主要总结下最近面试问到的问题,及自己理解的答案,不要纠结书写顺序,因为没有录音,所以想起什么就写什么。
 

数据结构

  1. 二叉树遍历方式
    ① 前序遍历:根节点——>左子节点——>右子节点
    ② 中序遍历:左子节点——>根节点——>右子节点
    ③ 后序遍历:左子节点——>右子节点——>根节点
    ④ 层序遍历:按层级从上到下,从左到右遍历。
  2. 已知数组通过下标查询速度为O(1),链表删除操作速度为O(1),自定义数据结构优化数组插入删除慢,链表查询速度慢问题(这个大家自己想下实现方式吧,每个人的思路不一样,咱们一起讨论下。我的方式是数组中存储链表节点,但这个方式没有完美解决这个需求,以后可能会总结完善了在写一个实现方式。)
  3. AQS的组成
     一个volatile修饰的变量state和两个成员属性节点node组成的虚拟双链表构成,通过state来判断锁状态。
  4. 延时队列delayQueue和时间轮算法的异同
    ① delayQueue是一个定时轮询的阻塞队列,每隔一段时间进行一次遍历来执行相应的任务
    ② 时间轮询算法是一个双向链表环,每个元素都是一个任务队列,不断循环这个链表来判断该任务队列是否要执行。

集合

  1. 基本逢面必问的HashMap实现原理我就不写了。
  2. HashMap 中的containsKey和ArrayList中的contains方法的时间复杂度分别是多少
    ① HashMap:O(1)
    ② ArrayList:O(n)
  3. ArrayList、HashSet、HashMap有什么共同性?
    ① ArrayList:数组实现
    ② HashMap:数组+链表+红黑树实现
    ③ HashSet:使用HashMap实现
    所以他们的共性就是都依赖数组实现。
  4. ArrayList什么时候扩容?扩容多少?
    ① ArrayList当新增元素个数为当前数组长度+1时进行扩容
    ② 扩容当前数组长度的1.5倍
  5. HashMap hash方法实现?
    ① key为null则直接返回0
    ② key不为null,判断key的hashCode是否与key向右位移16位相等,相等返回0,不等返回1
  6. ConcurrentHashMap如何保证线程安全
    ① JDK7使用:ReentrantLock + Segment + HahsEntry
    ② JDK8使用:Synchornized + CAS + HashEnty + 红黑树
    ③ 在多线程环境下,ConcurrentHashMap用两点来保证正确性:ForwardingNode和synchronized。
    ④ 当一个线程遍历到的节点乳沟时ForwardingNode,则继续向后遍历,如果不是则将该节点加锁,防止其他线程进入,完成后设置ForwardingNode节点,以便其他线程可以看到该节点已经处理过滤,如此交叉进行。
  7. 为什么放弃了分段锁
    ① 细化锁粒度提高并发效率
    ② Synchornized 比ReentrantLock更节约内存,优化空间更高
  8. HashMap的链表转换为红黑树后,它的根节点是什么颜色的
    黑色
  9. HashMap JDK 7 8的区别
    ① 数据结构由数组 + 链表 改为 数组 + 链表 + 红黑树
    ② 解决了多线程rehash过程中造成链表环的问题,主要是rehash过程不在进行倒排序,但还会存在元素丢失问题,所以它依旧不是线程安全的。
  10. HashMap的Hash方法中为什么要对数组长度-1
    ① hash方法的实现为key的hash值与length-1进行&运算,计算当前key 的存储位置,数组长度为2的n次幂可以减少hash冲突提高查询效率。
    ② 但是如果如果key的hahs值与length直接进行&运算,那么转化的二进制值最后一位都为0,那么如:00001,0011,0111,0101等这些位置就永远不可能存放元素了,造成了空间浪费的问题。
    ③ 由于浪费了其中几个数组中的位置,导致数组可用长度的降低等于是变相的提高了hash碰撞的几率,并且增加了红黑树的长度,降低了查询效率。
  11. HashMap中table的寻址原理,JDK7和JDK8有什么不同
    jdk7中使用indexFor方法来计算key在数组中的存储位置,jdk8中直接使用hash值与数组长度-1来计算存储位置并删除了indexFor方法
  12. HashMap为什么使用红黑树而不是平衡二叉树
    ① 红黑树和平衡二叉树查找时间复杂度都是O(log n),但红黑树调整平衡最多需要2次而平衡二叉树最多可能需要O(log n)
    ② 平衡二叉树在每个节点存储平衡因子,而红黑树存储当前节点颜色,比平衡因子更有用,因为红黑树永远不是一个空树
    ③ 平衡二叉树对于子树的平衡要求更严格,所以比红黑树的旋转更复杂,难以调试和实现。
    ④ 红黑树的增删查找操作都相对均衡,而平衡二叉树查询速度相对快些,但增删较慢

ThreadLocal:

  1. 实现原理
    将当前对象作为KEY,存入ThreadLocalMap中,ThreadLocalMap是ThreadLocal中的静态内部类,这里的getMap调用的是Thread类中的属性,用来保证每个线程都有一个独立的ThreadLocalMap防止线程间数据相互影响。
  2. 数据结构
    我们再接着看看ThreadLocalMap,跟想象中的Map有点不一样,它其实内部是有个Entry数组,将数据包装成静态内部类Entry对象,存储在这个table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode & table.length。只要数组的大小不变,这个索引下标是不变的,这也方便去set和get数据。
  3. 什么情况下会产生内存泄漏
    当在线程池中使用ThreadLocal时,因为ThreadLocalMap的key是一个弱引用,所以在一个线程在执行完毕后在下一次GC运行前没有再次执行任务,那么key将会被回收

线程池

  1. 主要参数
    ① corePoolSize:核心线程数大小。
        当线程池内的线程数小于该值时,每次任务提交创建一个新的线程,大于该值时新任务进入等待队列中。
    ② maximumPoolSize:最大线程数。
        当等待队列饱和后,每次创建新的线程执行任务,直至线程数达到该值,超过该值时,执行拒绝策略。
    ③ keepAliveTime:空闲时长。
        [1] 当前线程数大于核心线程数时,线程空闲时间达到该值时销毁,直至等于核心线程数。
        [2] 当completedAbruptly=true时,线程空闲时间达到该值时销毁,直至线程数为0。
    ④ unit:空闲时间单位。
        [1] TimeUnit.DAYS:天
        [2] TimeUnit.HOURS:小时
        [3] TimeUnit.MINUTES:分钟
        [4] TimeUnit.SECONDS:秒
        [5] TimeUnit.MILLISECONDS:毫秒
        [6] TimeUnit.MICROSECONDS:微妙
        [7] TimeUnit.NANOSECONDS:纳秒
    ⑤ workQueue:阻塞队列。
        [1] LinkedBlockingQueue:无界队列。需要注意,如果任务执行时间过长,阻塞队列内任务过多会导致OOM问题。
        [2] ArrayBlockingQueue:FIFO有界队列。
        [3] PriorityBlockingQueue:优先级有界队列。优先级由任务的Comparator决定。
        [4] SynchronousQueue:同步移交队列。它不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
    ⑥ threadFactory:线程工厂。
    ⑦ handler:拒绝策略。
        当阻塞队列饱和并且线程数到达最大线程数,或当调用shutdown方法后还有新的任务提交时执行。
        [1] AbortPolicy:拒绝接收新的任务,并抛出RejectedExecutionException异常。
        [2] DiscardPolicy:拒绝接收新任务,但不抛出异常。
        [3] DiscardOldestPolicy:移除等待队列栈顶元素,将新任务添加到队列中。
        [4] CallerRunsPolicy:拒绝接收新任务,将当前任务返回给调用者线程执行。
  2. 如何预创建线程等待执行任务
    ① prestartAllCoreThreads():初始化创建核心线程数个线程。
    ② prestartCoreThread():初始化创建一个线程。
  3. shutdown与shutdownNow的区别
    shutdown方法执行时,线程池处于SHUTDOWN状态,拒绝接受新的任务,当前所有任务执行后关闭线程池。
    shutdownNow方法执行时,线程池处于STOP状态,拒绝接受新的任务,并尝试结束当前执行的任务,清空等待队列。
  4. 如何为线程池内创建的线程设置统一名称:
    https://blog.csdn.net/woluoyifan/article/details/82861408

JVM

  1. JVM主要参数:
    ① -Xms:JVM初始化内存大小
    ② -Xmx:JVM最大可用内存大小
    ③ -Xmn:年轻代大小
    ④ -Xss:每个线程的堆栈大小
    ⑤ -XX:NewRatio:年轻代与老年代的比例值
    ⑥ -XX:SurvivorRatio:年轻代中Eden区与Survivor区的比例值
    ⑦ -XX:MaxPermSize:持久代大小
    ⑧ -XX:MaxTenuringThreshold:垃圾对象最大年龄
  2. GC Root 可达性分析:
    通过一系列称为“GC Roots”的对象作为起点,从这些结点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的,为可回收对象。
  3. 可作为GC Root根节点的对象有哪些
    ① JVM栈中引用的对象
    ② 方法区中类静态属性引用的对象
    ③ 方法区中常量变量引用的对象
    ④ 本地方法栈中引用的对象(Native方法中引用的对象)
    ⑤ 活跃线程的引用对象
  4. CMS执行过程
    ① 初始标记:标记GC Root可直接连接的对象,运行期间会停止其他用户操作
    ② 并发标记:在初始标记的基础上继续向下追溯标记,以所有已标记对象为根节点继续搜索向下标记。
    ③ 并发预清理:,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段”重新标记”的工作,因为下一个阶段会Stop The World。 
    ④ 重新标记 :停止其他用户操作,收集器线程扫描在CMS堆中剩余的对象。扫描从”根对象”开始向下追溯,并处理对象关联。
    ⑤ 并发清理 :清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。 
    ⑥ 并发重置:清理CMS栈。
  5. 内存模型
    ① 堆区:存放对象实例,几乎所有的对象实例都在这里分配内存。
    ② 方法区:被虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据。
    ③ JVM栈:每个方法被执行的时候都会同时创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口。
    ④ 本地方法栈:线程高速缓存。
    ⑤ 程序计数器:每条线程的程序计数器,相互独立,线程私有的。
  6. G1运行机制
    ① 初始标记:仅标记GC Roots能直接到达的对象,并且修改TAMS的值,让下一阶段用户程序并发运行时能在正确可用的Region中创建新对象。
    ② 根区域扫描:从GC Roots开始对对已标记的引用扫描,并标记对老年代的引用。
    ③ 并发标记:在整个堆中查找可访问的对象并标记。
    ④ 最终标记:多线程修正在并发期间因用户程序执行而导致标记产生变化的标记。切将对象的变化记录在Remembered Set Logs里,把这里的数据合并到Remembered Set中。
    ⑤ 筛选回收:对每个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来指定回收计划。
  7. 对象创建后的晋升过程(包含GC运行过程)
    ① 对象创建出来后,如果是大对象则直接分配到老年代,可通过-XX:PretenureSizeThreshold进行设置
    ② 此时年轻代划分为三个部分,Eden区、S0、S1,对象正常进入Eden区,如果Eden区满了则进入S0
    ③ 如果S0满了则进行一次Young GC,此时通过GC Roots进行可达性分析并标记
    ④ 然后将标记对象copy到S1中,所有存活对象年龄 + 1,清理Eden区和S0,然后S1替换S0。
    ⑤ 如果S1中的存活对象相同年龄的对象超过一半时,则将大于等于此年龄的对象直接晋升到老年代中。
    ⑥ 如果对象年龄达到一定数值则自动晋升到老年代中,可通过XX:MaxTenuringThreshold进行设置。
    ⑦ CMS ParNew 执行过程就不写了。
  8. CAS原理
    CAS操作都是通过sun包下Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,主要原理就是将一个旧值与目标值进行比较如果相同就将目标值修改为新值。
  9. volatile作用及实现方式
    ① 确保变量的内存可见性
    volatile修饰的变量保存在JVM内存中,当有新的线程需要使用这个变量时会将此变量拷贝到当前线程的高速缓存中使用,当有读写操作时,线程会到JVM内存中进行嗅探内存地址中的值是否有发生改变,如果有改变则将高速缓存中的内存地址修改为失效状态,然后重新从JVM中读取使用
    ② 禁止指令重排序
    JVM在编译运行一段代码时会对一段代码进行优化来提高系统运行效率,但当一个变量被volatile修饰后,JVM会放弃对这个变量相关的读写操作进行优化,其中涉及到happens-before原则。
    ③ 指令重排序
    JVM会对代码执行顺序进行优化,以提高代码的运行效率,保证最终执行的结果与程序顺序执行的结果一致,它只会对不存在数据依赖性的指令进行重排序。因为,在单个县城中程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则只是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。
    ④ happens-before原则:
        【1】程序次序规则:
            [1] 一个线程内,按照代码顺序执行,书写在前面的操作先行发生于书写后面的操作。
            [2]一段程序代码的执行在单个线程中看起来是有序的。虽然这条规则中提到书写在前面的操作先行发生于书写在后面的操作,这个应该是看起来执行的顺序是按照代码顺序执行的,但JVM可能会对程序代码进行指令重排序。
        【2】锁定规则:
            [1] 一个unLock操作发生于后面对同一个锁操作无论在单线程中还是多线程中,同一个锁如果处于被锁定的状态,那么必须先对锁进行了释放操作,后面才能进行lock操作。
            [2] volatile变量规则:对一个变量的鞋操作先行发生于后面对这个变量的读操作。
        【3】传递规则:
            如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出A先行发生于操作C。
        【4】线程启动规则:
            Thread对象的start方法先行发生于此线程的每一个动作。
         【5】线程中断规则:
            对线程interupt方法的调用先行发生于被中断线程的代码检测到中断时间的发生。
         【6】对象终结规则:
            一个对象的初始化完成先行发生于他的finalize方法的开始。
  10. synchronized原理及实现方式,修饰静态方法、普通方法、代码块的区别
    synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性
    ① synchronized修饰静态方法:
    当它修饰静态方法时,锁定的是当前的类对象,所有线程请求到达后先去尝试获取当前类对象的锁,如果没有获取到则阻塞。
    ② synchronized修饰方法:
    当它修饰普通方法时,通过方法修饰符上的ACC_SYNCHRONIZED标志位实现,在class文件的方法表中将该方法的access_flags字段中的synchronized标志位置为1,表示该方法时同步方法并使用调用该方法的对象或该方法所属的class在JVM的内部对象表示class作为锁对象。
    ③ synchronized修饰代码块
    同步代码块时使用monitorenter和monitorexit指令实现。monitorenter指定插入到同步代码块开始的位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之关联,当一个monitor被持有之后,它将处于锁定钻头。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即阐释获取目标对象锁。
  11. GC运行日志解读
    [GC(Young GC) 5.617(时间戳): [ParNew(使用ParNew作为年轻代的垃圾回收期): 43296K(年轻代垃圾回收前的大小)->7006K(年轻代垃圾回收以后的大小)(47808K)(年轻代的总大小), 0.0136826 secs(回收时间)] 44992K(堆区垃圾回收前的大小)->8702K(堆区垃圾回收后的大小)(252608K)(堆区总大小), 0.0137904 secs(回收时间)] [Times: user=0.03(Young GC用户耗时) sys=0.00(Young GC系统耗时), real=0.02 secs(Young GC实际耗时)]  
  12. 导致线程死锁的原因?怎么解除线程死锁。
    ① 造成死锁的原因:
        [1] 多个线程持有锁标记的同时请求其他锁标记
        [2] 线程获取锁后一直没有释放
    ② 如何解除死锁或避免
        [1] 使用volatile修饰对象禁止指令重排序,使所有锁按顺序执行。
        [2] 加锁限时:当一个线程没有在指定时间内获取所有的锁标记,则回退释放当前获取的所有锁标记。
        [3] 死锁检测:当一个线程获取锁超过指定时间,则回退并释放当前锁标记。
  13. 不同机器间的多线程请求如何保证协调性
    使用redis作为分布式锁
  14. 多线程如何保证顺序执行
    ① 使用信号量Semaphore
    ② Object的wait和notify方法
    ③ 使用volatile修饰的变量作为同步标识
    ④ Thread的isAlice方法判断线程是否执行结束
  15. 排查一次线上JVM问题(OOM问题)
    ① 使用top命令查看哪个进程占用率过高
    ② 然后查看具体是哪个线程出现问题进行定位:ps -mp PID -o THREAD ,tid,time
    ③ 将TID转为16进制并查看线程信息:printf "%x\n" TID   输出结果为XXX
    ④ jstack PID | grep XXX -A30 查看线程信息
    ⑤ 然后根据线程信息进行下一步分析:jstat -gcutil PID 间隔时间M 次数N 查看线程GC运行情况
    ⑥ 频繁FullGC并且回收前后的内存大小差别不大,进一步进行堆栈分析
    ⑦ jmap -dump:format=b,file=a.dta [pid]
    ⑧ 如果使用eclipse的话可以使用它的Eclipse Memory Analyzer中生成Leak Suspects报告。IDEA可以使用JProfiler插件
    ⑨ 最终定位代码大致位置进行代码排查。
  16. JVM 在JDK7 8的区别
    去除了永久代,新增了元空间,元空间使用的是本地内存,
    所以它的大小除了-XX:MetaspaceSize(初始化大小)和-XX:MaxMetaspaceSize(最大值,默认无限制)两个参数外,也受到本地内存大小的限制。
  17. Integer类型
    ① Integer a = new Integer(1); Integer b = new Integer(1);  a == b 返回什么
    返回false,因为两个对象的内存地址不同;
    ② Integer a = 1,b = 1; a == b返回什么
    返回true,自动封箱拆箱比较的是值
    ③ Integer a = 200,b = 200; a == b 返回什么
    返回false,Integer 缓存-127 ~ 128之间的数值进行缓存做封箱拆箱操作,超过范围的不缓存。

 Dubbo

  1. Dubbo的主要组成部分
    ① Provider:服务提供方
    ② Consumer:服务消费方
    ③ Registry:注册中心
    ④ Monitor:监控中心
    ⑤ Container:运行容器
  2. Dubbo协议头数据内容:
    2byte magic:类似java字节码文件里的魔数,用来判断是不是dubbo协议的数据包。魔数是常量0xdabb
    1byte 的消息标志位:16-20序列id,21event,22two way,23请求或响应标识
    1byte 状态,当消息类型为响应时,设置响应状态。24-31位。状态位,设置请求响应状态,dubbo定义了一些响应的类型。具体类型见com.alibaba.dubbo.remoting.exchange.Response
    8byte 消息ID,long类型,32-95位。每一个请求的唯一识别id(由于采用异步通讯的方式,用来把请求request和返回的response对应上)
    4byte 消息长度,96-127位。消息体 body 长度,int类型,即记录BodyContent有多少个字节。
  3. Dubbo支持哪些协议
    ① Dubbo协议
    ② Memcached协议
    ③ Redis协议
    ④ HTTP协议
    ⑤ WebService协议
    ⑥ TCP协议
    ⑦ Thrift协议
    ⑧ Hessian协议
  4. http协议与dubbo协议的区别
    这个问题回答的点就比较多了,看你了解哪部分就扯哪部分好了。反正我就说了一丢丢不知道对不对的东西,然后跟他扯别的转移话题了。如果对这里比较了解的兄弟记得分享下啊,留言给我。
  5. RPC调用过程
    ① client以本地调用的方式发起调用。
    ② client stub (客户端存根)收到调用后,负责将被调用的方法名、参数等打包编码成特定格式的能进行网络传输的消息体。
    ③ client stub将消息体通过网络发送给服务端。
    ④ server stub(服务端存根)收到通过网络接收到消息后按照相应格式进行拆包解码,获取方法名和参数。
    ⑤ server stub根据方法名和参数进行本地调用。
    ⑥ 被调用者本地调用执行后将结果返回给server stub。
    ⑦ server stub将返回值打包编码成消息,并通过网络发送给客户单。
    ⑧ client stub收到消息后,进行拆包解码,返回给client。
    ⑨ client得到本次RPC调用的最终结果。

MySQL

  1. MySQL InnoDB索引:B+Tree
  2. MySQL中,两个字段的联合索引是否要按顺序书写:不需要
  3. MySQL中,向一个表中添加一条数据当前ID是1,然后回滚,在插入一条数据,这条数据的ID是多少:3
    因为回滚操作不会回滚自增主键计数器的值,而且回滚事物执行时为了防止不影响后序事物的执行,所以会占用一次自增主键的值,所以新插入的主键值为3。
  4. MyIsam索引:B+Tree
  5. MyIsam索引与InnoDB索引的区别(主键索引)
    MyIsam索引的叶子节点保存的是数据的内存地址,InnoDB叶子节点保存的直接就是数据
  6. 事务隔离级别
    ① 读未提交:可以读取其他事物未提交的数据,可能存在脏读问题(一个事物读取了另一个事物未提交的数据)。
    ② 读 提 交  :只读取其他事物已提交的事物,可能会存在不可重复读问题(对一条数据的多次读取数据不一致)。
    ③ 可重复读:在一个事物内的查询都与事物开始时读取一致,可能会存在幻影读(一个事物读取某个范围的数据时,另一个事物在这里进行了写操作导致数据与预期不一致)。
    ④ 串 行 化  :所有事物按顺序执行,效率低下。
  7. mysql间隙锁,已知表中只有一个字段id是主键,有数据 1。3。10三条数据,此时执行两个事物
    事物A:delete 4  事物B delete 5  事物A insert 6  事物B insert 7 当前两个事物都还没有commit问这个过程是否有阻塞,如果有说明阻塞在哪个命令位置,如果没有请说明原因。
    首先,delete 4 5 不会阻塞后续操作,因为他们的影响行为0,此时事物A插入一个id为6的数组会锁定上下两条数据间的空间,即3和10两条中间的位置,所以事物B的insert 7会阻塞等待事物A执行commit后才可继续插入。
  8. 从一个班级表中,找出分数大于80,且大于三门功课的学生姓名
    select user_name from grades_in_class where grades > 10 group by user_name having count(user_name) >= 3

  9. MyBatis如何动态切换数据源
    ① 配置多个SqlSessionFactory
    ② 配置多个DataSource
    ③ 创建自定义注解添加到对应的接口上作为切入点
    ④ 配置切面根据请求类型配置对应的DataSource

  10. mysql中 in 和 exist的使用
    ① 当内查询结果集小于外查询时使用in来进行查询
    ② 当内查询结果集多余外查询时使用exists进行查询
    ③ 当使用exists时会对外表做循环查询判断,in只对外表查询一次
    ④ sql语句最大长度为1M,所以当in的参数过多时将报错

GIT

  1. 指定合并另一个分支的某一次commit到当前分支上使用什么命令:git cherry-pick

Linux

  1. 查看当前CUP:top命令
  2. 查看当前线程:jstake pid
  3. 打印堆栈信息:jmap -dump:format=b,file=filename pid
  4. 文本分析命令:awk
  5. 查看网络连接状态:netstat -an

Redis

  1. 数据类型
    ① string:使用sds实现。内部buf数组使用空间预分配策略实现,由len记录已使用字节数、free记录未使用字节数,当value长度小于1M时每次翻倍扩容,大于1M时每次增加1M,最大长多512M。
    ② list:通过链表实现。
    ③ hash:当内部所有键值对数量小于512并且所有键值长度小于64字节时使用ziplist,否则使用hashtable实现。采用渐进式rehash实现扩容。
    ④ set:无序集合类似hashset实现。
    ⑤ zset:有序集合,通过给每个元素指定score来代表排序权重(升序)
    ⑥ bitmap:位图并不是一个真正的数据类型,而是基于string类型的一系列位操作实现位图功能
    ⑦ hyperloglog:提供不精确的去重方案,误差范围在0.81%左右。
  2. 渐进式rehash策略
    触发resize条件后,置isRehashing=true,在rehashing期间,每个操作put,get等操作都顺便把相应的entry迁移,并且有后台的线程也在分批次进行迁移。get时先getHT[0] 没找到在getHT[1],迁移结束后,HT[0]指向新的hash table 置isReHashing为false
  3. 持久化
    ① RDB:
        通过fork产生子进程,父进程继续处理client请求,子进程负责将快照写入临时文件中,完成后用临时文件替换原有的快照文件。
    ② AOF: 
        通过fork产生子进程,父进程继续处理client请求,子进程把AOF内容写入缓冲区,子进程写完退出,父进程接收到退出消息后,将缓冲区AOF写入临时文件,覆盖原文件。
    ③ AOF文件生成过程:
        [1] 命令传播:redis将执行完的命令、参数等信息发送到AOF程序中
        [2] 缓存追加:根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的AOF缓存中。
        [3] 文件写入:AOF缓存中的内容写入AOF文件末尾,如果AOF保存条件被返祖的话,调用fsync或fdatasync将内容真正保存到磁盘。
    ④ sava 和 bgsave的区别:
        [1] save直接调用rdbSave,阻塞redis主进程,直到保存完成为止。
        [2] bgSave,通过fork创建一个子进程,子进程负责调用rdbSave,并在保存完成之后想主进程发送信号进行通知。
    ⑤ fsync与fdatasync的区别:
        除了都会同步文件的修改内容(脏页),fsync还会同步文件的描述信息(metadata,包括size,访问时间等等),因为文件的数据和metadata通常存在硬盘的不同地方,因此fsync至少需要两次IO写操作。而fdatasync只影响文件的数据部分。
  4. 键过期处理:redis所有的数据结构都可以设置过期时间,时间到了会自动删除。redis会将每个设置了过期时间的key放入一个独立的字典中,以后会定时遍历这个字典来删除到期的key。默认使用惰性删除+定期删除结合的策略。
    ① 定时删除:在设置key过期时间的同时创建一个定时器,让定时器在key过期时执行删除命令。
    ② 惰性删除:仅在每次收到对key的请求时才检查是否阔气,如果已过期则删除key。
    ③ 定期删除:每隔一段时间对key字典进行检查,删除过期的key。
  5. 键踢出策略:当redis内存超出无力内存限制时,内存的数据会开始和磁盘产生频繁的交换,交换会让redis的性能急剧下降。所以为了限制最大使用内存,redis提供了配置参数maxmemeory来限制内存超出期望大小的处理策略。
    ① Noeviction:不踢出,当内存达到阈值时,所有引起申请内存的命令都会报错。
    ② volatil-lru:在设置了过期时间的键空间中,优先移除最近为使用的key,如果键空间为空则报错。
    ③ volatile-ttl:在这只了过期时间的键空间中,优先移除过期时间最近的key,如果键空间为空则报错。
    ④ volatile-random:在设置了过期时间的键空间中,随机移除一个key,如果键空间为空则报错。
    ⑤ allkeys-lru:在主键空间中,优先移除最近未使用的key。
    ⑥ allkeys-random:在主键空间中,随机移除某个key。
  6. 主从复制:
    ① 全量同步:在主库上进行一次bgsave将当前内存的数据快照保存到磁盘文件中,然后将快照文件的内容全部传输到从节点。从节点将快照文件接收完毕后,立即执行一次全量加载,加载之前先要将当前内存的数据清空。
    ② 部分同步:只同步从服务器中没有的数据,设计到赋值偏移量和赋值挤压缓冲区。
    ③ 命令传播:用于在master的数据库状态被修改时,将导致变更的命令传播给slave,从而让slave的数据库状态与master保持一致。
  7. 命令指派:
    ① 客户端向服务器发送数据库键命令
    ② 集群节点计算槽点负责节点是否由自己负责
    ③ 如果是则执行当前命令,如果不是则返回MOVED错误,及计算结果给客户端
    ④ 客户端通过异常处理,并根据MOVED提供的信息跳转到正确的节点执行命令
  8. 最久未使用踢出策略:LRU算法
    ① 新数据插入到链表头部
    ② 每次访问命中缓存,则将数据移动到链表头部
    ③ 链表满时,移除链表尾部元素
  9. 超时策略LRU-K
    ① 数据第一次被访问时,加入到访问历史列表
    ② 如果数据数据在访问历史列表后没有达到K次访问,则按照规则淘汰(FIFO,LRU)
    ③ 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列转移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序
    ④ 需要淘汰数据时,淘汰缓存队列中排在末尾的数据
  10. 哨兵
    ① 每个哨兵每秒想它所知的master、slave及其他哨兵发送ping命令,检查存活状态
    ② 如果一个实例距离最后一次有效回复ping命令时间超过预设值(down-after-milliseconds),则这个实例会被哨兵标记为主观下线
    ③ 如果一个master被标记为主观下线,则正在监视这个master的所有哨兵要每秒确认一次master的确进入了主观下线状态
    ④ 当有足够的哨兵在制定的时间范围内确认master的确进入了主观下线状态,则master会被标记为客观下线
    ⑤ 一般情况下,每个哨兵会以每10秒一次的频率想它所制定的所有master、slave发送info命令
    ⑥ 当master被哨兵标记为客观下线时,info命令的频率改为每秒一次
    ⑦ 若没有足够数量的哨兵同意master已经下线,则master的客观下线状态会被移除
    ⑧ 如果有足够数量的哨兵同意master已下线,则进行投票选举master,当被投slave的票数达到 n/2+1时,成为新的master
  11. sds内部实现
    主要通过三个成员变量实现
    ① 保存数据的数组
    ② 保存当前数组已使用长度
    ③ 保存当前数据剩余可使用数量
  12. redis事物实现方式与原理
    redis的事物主要通过命令打包实现,同时将多个命令封装成一个原子操作,在此操作未结束前会阻塞客户端的其他操作。其中WATCH命令与其他命令不同,在每个代表数据库的redis.h/redisDb结构类型中,都保存了一个watched_keys字典,字典的键时这个数据库被监视的键,而字典的值则是一个链表,链表中保存了所有监视这个键的客户端。当有其他客户端修改watch_keys字段中的key时,关于关联的所有client对象的REDIS_DIRTY_CAS标志被打开,当客户端发送EXEC命令时会检查client对象的REDIS_DIRTY_CAS是否被打开,如果被打开则事物执行失败。
  13. 如何保证两个redis方法调用,使用redis保证他们的原子性
    使用setnx方法,其原理与synchronized类似,通过标志位的值来判断锁状态从而达到加锁目的。注意,释放锁标志的操作一定要放在finally中防止死锁。
  14.  

Spring

  1. 请求处理流程
    ① 客户端发送一个http请求给web服务器,web服务器对http请求进行解析,如果匹配DispatcherServlet在web.xml中的请求映射路径,则将请求转交给DispatcherServlet
    ② DispatcherServlet接收到这个请求之后根据请求的信息(URL/HTTP方法/请求的报文头/cookie和请求参数等信息)以及HandlerMapping的配置找到请求的Handler,将处理权交给Handler。
    ③ Handler将具体的处理进行封装,由具体的HandlerAdapter地Handler进行具体的调用
    ④ Handler对数据处理完成以后将返回一个ModelAndView对象给DispatcherServlet
    ⑤ 由于ModelAndView时一个逻辑视图,所以DispatcherServlet会通过ViewResolver将逻辑视图转换为真正的视图
    ⑥ Dispatcher通过model解析ModelAndView中的参数,最终展现出完整的view并返回给客户端
  2. IOC:操作反射为成员变量动态赋值
  3. AOP:通过代理实现面向切面编程。
  4. JDK动态代理与cglib的区别及优缺点
    ① jdk通过实现目标接口实现,cglib通过继承目标类实现
    ② JDK动态代理是利用反射生成一个实现代理类接口的匿名类,在调用具体方法前先调用InvokeHandler进行处理;不需要依赖外部,但只能基于接口进行代理。
    ③ cglib时利用ASM开源包将对象类的class文件加载进来,通过修改其字节码生成子类进行处理;无论代理对象有没有实现对应的接口均可代理,但不能处理final修饰的方法。
    ④ Spring默认情况下使用JDK动态代理,如果目标不是接口则更换为cglib,如果指定代理模式为true则全部使用cglib。
  5. Spring事物传播机制
    ① PROPERGATION_MANDATORY :方法必须运行在一个事务中,不存在事务则抛出异常
    ② PROPERGATION_NESTED :存在事务则运行在嵌套事务中,不存在则创建一个事务
    ③ PROPERGATION_NEVER :当前方法不能运行在事务中,存在事务则抛出异常
    ④ PROPERGATION_NOT_SUPPORT :当前存在事务则将其挂起
    ⑤ PROPERGATION_REQUIRED :不存在事务则创建一个事务
    ⑥ PROPERGATION_REQUIRES_NEW :新建一个自己的事务,不论当前是否存在事务
    ⑦ PROPERGATION_SUPPORT :存在事务则加入,不存在也可以
  6. Spring中应用了哪些设计模式
    ① 简单工厂模式:在spring.xml中通过配置bean标签的方式创建的bean对象
    ② 工厂模式:交给spring管理的自定义类
    ③ 单例模式:通过作用域配置,spring中单例模式的bean被单独放在一个BeanFactory中进行管理,并提供全局访问
    ④ 适配器模式和代理模式都是AOP形式使用。
    ⑤包装器模式:动态的给对象增加一些额外功能。
    ⑥ 观察者模式:定义对象间的一对多关系,当这个对象更新时,所有依赖它的对象都会得到通知,如ApplicationListener
    ⑦ 策略模式:定义一系列的算法并将其封装,使其可以随意相互替换。
    ⑧ 模板方法模式:继承覆盖指定方法或使用回调形式进行修改代码片段。

  7. @Resource和@Autowird的区别
    ① resource使用名字匹配,当没有找到对应Bean时在使用Type匹配,Autowird使用Type匹配
    ② resource时J2EE的注解,Autowird是Spring的注解

排序

  1. 冒泡
         public static void main(String[] args) {
            Integer[] array = {3,5,1,-7,4,9,-6,8,10,4};//用Integer主要是方便打印,不打印建议还是用int节约内存
            int length = array.length - 1;//因为每次判断都会重新计算,所以提取出来
            for (int i = 0; i < length; i++){
                for (int j = 0; j < length - i; j++) {
                    if (array[j] > array[j+1]) {
                        int temp = array[j];
                        array[j] = array[j+1];
                        array[j+1] = temp;
                    }
                }
            }
            System.out.print(Joiner.on(",").join(array));
        }
  2. 快排
    public static void main(String[] args) {
            Integer[] array = {3,5,1,-7,4,9,-6,8,10,4};//用Integer主要是方便打印,不打印建议还是用int节约内存
            quickSort(array, 0, array.length - 1);
            System.out.print(Joiner.on(",").join(array));
        }
        private static void quickSort(Integer[] array, int low, int high) {
            if (low < high) {
                int index = getIndex(array, low, high);
                quickSort(array, 0, index - 1);
                Arrays.asList(array).forEach(item -> System.out.print(item + "\t"));
                System.out.println();
                quickSort(array, index + 1, high);
            }
        }
    
        private static int getIndex(Integer[] array, int low, int high) {
            int tmp = array[low];
            while (low < high) {
                while (low < high && array[high] >= tmp) {
                    high--;
                }
                array[low] = array[high];
                while (low < high && array[low] <= tmp) {
                    low++;
                }
                array[high] = array[low];
            }
            array[low] = tmp;
            return low;
        }
    代码最少的快排
     
        public static void main(String[] args) {
            Integer[] ints = {1, 23, 3, 6, 12, 7, 8, 9, 4, 23, 0};
            Integer[] ints1 = sortQuick(ints, 0, ints.length - 1);
            System.out.print(Joiner.on(",").join(ints1));
        }
        public static Integer[] sortQuick(Integer[] in, int left, int right){
            int key = in[left];
            int start = left;
            int end = right;
            while(start<end){
                while(start < end && key<=in[end]){//从右向左遍历,找到小于key的,放入下标strat中。
                    end--;
                }
                in[start] = in[end];
                while(start < end && key > in[start]){//从左向右遍历,找到大于key的,放入下标end中。
                    start++;
                }
                in[end] = in[start];
            }
            in[start] = key;//此时start==end,这就是所谓的轴,把key放入轴中,轴左边的都<key,轴右边的都>key
            if(start>left)sortQuick(in,left,start-1);//此时要对这两部分分别进行快排
            if(end<right)sortQuick(in, end+1, right);
            return in;
        }
  3. 二叉树遍历
    //前序遍历
    public void preOrder(TreeNode node){
        if(node != null){
            System.out.print(node.getValue() + "\t");
            preOrder(node.left);
            preOrder(node.right);
        }
    }
    //中序遍历
    public void preOrder(TreeNode node){
        if(node != null){
            preOrder(node.left);
            System.out.print(node.getValue() + "\t");
            preOrder(node.right);
        }
    }
    //后续遍历
    public void preOrder(TreeNode node){
        if(node != null){
            preOrder(node.left);
            preOrder(node.right);
            System.out.print(node.getValue() + "\t");
        }
    }
    

     

单例模式

最近两天碰到的大神都有相关的解释为什么是线程不安全的,由于我自己还没有理解所以先不写了,防止误导大家,感兴趣的可以自己研究下然后大家在评论区一起分享下研究成果。

  1. 静态内部类(调用效率高,可以延时加载
    public class SingletonDemo {
        private static class A{
            private static final SingletonDemo instance=new SingletonDemo();
        }  
        private SingletonDemo(){}
        public static SingletonDemo getInstance(){
            return A.instance;
        }   
    }
  2. 懒汉式(调用效率不高,但是能延时加载)

    public class SingletonDemo{
    	private static volatile SingletonDemo sd;
    	private SingletonDemo(){}
    	public static SingletonDemo getSingletonDemo(){
            if(sd == null){
                synchornized(sd){
                    if(sd == null){
                        sd = new SingletonDemo();
                    }
                }
            }
    		retrun sd;
    	} 
    }
  3. 饿汉式(调用效率高,但是不能延时加载)
     

    public class SingletonDemo{
    	public static final SingletonDemo sd = new SingletonDemo();
    	private SingletonDemo()
    	SingletonDemo getSingletonDemo(){return sd;}
    }

手写代码

  1. 求二叉树最下层最左节点(如果最下层只有一个右节点,那么这个节点也算最左节点)
    图中最左节点为51
        /**
         * 静态内部类,树基础结构对象
         */
        private static class Node{
            // 当前节点值
            private int value;
            // 左节点
            private Node left;
            // 右节点
            private Node right;
        
            public Node() {
            }
        
            public Node(int value) {
                this.value = value;
            }
        
            public Node(int value, Node left, Node right) {
                this.value = value;
                this.left = left;
                this.right = right;
            }
        
            public int getValue() {
                return value;
            }
        
            public void setValue(int value) {
                this.value = value;
            }
        
            public Node getLeft() {
                return left;
            }
        
            public void setLeft(Node left) {
                this.left = left;
            }
        
            public Node getRight() {
                return right;
            }
        
            public void setRight(Node right) {
                this.right = right;
            }
        }
    // 树型基础构建方法
    private static Node createNode(){
            return new Node(1,
                    new Node(21,new Node(31),new Node(32,new Node(41),null)),
                    new Node(22,null,new Node(33,new Node(42,null,new Node(51)),null)));
        }
        // 最低层级
        private static int level = 0;
        // 最终值
        private static int val = 0;
        private static void lestLeftNodeVal(Node node,int thisLevel){
            if(node != null){
                Node left = node.getLeft();
                Node right = node.getRight();
                if(left == null && right == null){
                    if(thisLevel > level){
                        val = node.getValue();
                        level = thisLevel;
                    }
                }
                if(left != null){
                    lestLeftNodeVal(left, thisLevel + 1);
                }
                if(right != null){
                    lestLeftNodeVal(right, thisLevel + 1);
                }
            }
        }
     
        public static void main(String[] args) {
            lestLeftNodeVal(createNode(),1);
            System.out.println(val);
        }
  2. 求随机一个最小局部变量,要求时间复杂度小于O(n)(数组中的元素不一定就是下面的数据,只是一个简单示例)
    求元素小于相邻两个元素,如果在头部或尾部则只小于相邻一个元素即可
    图中 1,2,7为正确答案
     
    private static int find(int[] array,int min,int max){
            int mid = (min + max) / 2;
            if(mid == 0 || mid + 1 >= max){
                return array[mid];
            }
            if(mid - 2 < 0 || (array[mid - 2] > array[mid - 1] && array[mid - 1] < array[mid])){
                return array[mid - 1];
            }
            if(array[mid - 1] > array[mid - 2]){
                return find(array,min,mid);
            } else {
              return find(array,mid,max);
            }
        }
    public static void main(String[] args) {
            int[] array = {1,3,2,5,6,9,8,7};
            int[] array2 = {1,3,4,5,6,9,8,7};
            int[] array3 = {9,8,7,6,5};
            int[] array4 = {1};
            System.out.println(find(array4,0,array4.length));
        }
  3. 给定两个链表,将两个单链表相同位置相加超过10进一位,要求返回这个相加结果的新链表
    public static void main(String[] args) {
            Node node1 = new Node(5, new Node(8, new Node(0)));
            Node node2 = new Node(3, new Node(2));
            int sum = 0;
            Node node = new Node();
            while (true){
                if(node1 == null && node2 == null){
                    break;
                }
                Node newNode = new Node();
                if(node1 != null){
                    sum += node1.getValue();
                    node1 = node1.getNext();
                }
                if(node2 != null){
                    sum += node2.getValue();
                    node2 = node2.getNext();
                }
                if(sum > 9){
                    newNode.setValue(sum % 10);
                    sum = 1;
                }else {
                    newNode.setValue(sum);
                    sum = 0;
                }
                Node n = node;
                if(node.getNext() != null) {
                    do {
                        n = n.getNext();
                    }while (n.getNext() != null);
                }
                n.setNext(newNode);
            }
            // 打印以上逻辑结果
            do {
                System.out.println(node.getNext().getValue());
            }while (node.getNext() != null);
        }

     
  4. 给定一个链表,和一个指定数,求根节点到叶子链路上的值相加等于指定数字
    假设有下图树形结构,目标值为12,输出单链表 1->2->4->5  而 1->3->6->2则不是因为最后一个节点2还有子节点。

  5. 12345反转为54321,不允许转成字符串(下面考虑了参数为负数的可能性,并进行了相应转换)
     public static void main(String[] args) {
            System.out.println(flip(12345));
        }
    
        public static int flip(int num){
            Long b = 0L;
            while (num > 0){
                b = b * 10 + num % 10;
                num /= 10;
            }
            return b > Integer.MAX_VALUE || b < Integer.MIN_VALUE ? 0 : b.intValue();
        }
  6. 单链表翻转
    public static void main(String[] args){
        Node linked = new Node();//假如这是 a-b-c-d的单链表
        Node next = null,newLinked = null;
        while(linked != null){
            next = linked.next;
            if(newLinked == null){
                newLinked = linked;
                linked.next = null;
            }else{
                linked.next = newLinked;
                newLinked = linked;
            }
            linked = next;
        }
    }
  7. 单链表 检测是否有环
     
    public static void main(String[] args){
        Node node3 = new Node(4, false, null);
        Node node = new Node(1, false, new Node(2, false, new Node(3, false, node3)));
        node3.next = node;
        Node node2 = node;
        while (node2.next != null){
            if(node2.next.sign){
                System.out.println("有环");
                break;
            }
           node2.next.sign = true;
           node2 = node2.next;
        }
    }
  8.  

TCP-IP

  1. TCP连接状态
    ① ESTABLISHED:正在通信
    ② TIME_WAIT:主动关闭
        【1】定义:表示主动关闭,时主动关闭连接的一方保持的状态,一般会保持这个状态2MSL(max segment lifetime)时间之后,彻底关闭,回收资源。乳沟在这个期间再次bind这个端口,就会出现Address already in use错误。
        【2】作用:
            [1] 可靠的时限TCP全双工连接的终止:
            如果服务器最后发送的ACK因为某种原因丢失了,那么客户端一定会重新发送FIN,这样因为有TIME_WAIT的存在,服务器会重新发送ACK给客户端,如果没有TIME_WAIT,那么无论客户端有没有收到ACK,服务器都已经观点链接了,此时客户端重新发送FIN,服务器将不会发送ACK,而是RST,从而使客户端报错。也就是说TIME_WAIT有助于可靠的时限TCP全双工链接的终止。
            [2] 允许老的重复分节在网络中消逝:
            如果没有TIME_WAIT,我们可以在最后一个ACK还未到达客户端的时候就简历一个新的链接。那么此时,如果客户端收到了这个ACK的话,可能会导致双方链接错乱,必须保证这个ACK完全死掉之后,才能简历新的链接。也就是说TIME_WAIT允许老的重复分节在网络中的消逝。
    ③ CLOSE_WAIT:被动关闭
发布了110 篇原创文章 · 获赞 475 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/yongqi_wang/article/details/102632279
今日推荐