你好,相信你一定很期待这份面试题,这份面试题是我花数个月整理出来的,内容涵盖了各方各面,特别是JVM、Mysql、多线程高并发、Redis、Spring这几个专题,是博主精心整理的亲身被面过的阿里、京东、虾皮、蚂蚁等一些大厂的面试题,学会进大厂真的不是梦!

精彩内容截图:





光看截图看不够?别急,下面即将解锁这200道面试题的题目与答案,想进大厂?靠它准没错!

文章目录

  • 1、面试题
    • 1、JVM
    • 2、集合
    • 3、多线程
    • 4、Redis
    • 5、消息队列
    • 6、Mysql
    • 7、Spring
    • 8、Mybatis
    • 9、设计模式
    • 10、计算机网络
    • 11、数据结构
    • 12、其它
    • 13、情景题
    • 14、IO
    • 15、分布式与高并发
  • 2、笔试题

1、面试题

1、JVM

  1. JVM有哪些调优参数?

    -Xms1024m -Xmx2048m -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m
    打印GC日志:-XX:+PrintGC、-XX:+PrintGCDetails
    -XX:+UseG1GC
    设置Eden区大小和Survivor区大小的比例:-XX:SurvivorRatio,默认是8:1:1
    年轻代和老年代的比例:-XX:NewRatio,默认是1:2
    
  2. 有哪些垃圾回收器,讲一个?

    针对年轻代:
        Serial串行收集器,暂停用户线程,单个GC线程工作
        ParNew并行收集器,实质上是Serial收集器的多线程并行版本
        Parallel Scavenge收集器:关注点是吞吐量高
    只针对老年代:
        Serial Old是Serial收集器的老年代版本
        Parallel Old收集器是Parallel Scavenge的老年代版本
        CMS:停顿时间短
    年轻代和老年代都适用:
        G1:面向局部收集,基于Region的内存布局,只要收集的速度能跟得上对象分配的速度,不追求一次把整个Java堆全部清理干净,G1将Region作为单次回收的最小单元(JDK1.9的默认回收器)
    
  3. JVM内存区域划分

    程序计数器(线程私有)

    本地方法栈

    虚拟机栈(线程私有)

    虚拟机堆(线程共享)

    方法区(线程共享)

  4. 垃圾回收算法原理?

    标记复制法(每次触发GC时扫描Eden区和SurvivorFrom区,并将存活下来的对象放到SurvivorTo区)

    标记清除法(产生内存碎片)

    标记整理法

    备注:年轻代的GC叫MinorGC和YoungGC,老年代或整个堆区的GC叫MajorGC或FullGC。

  5. 标记复制法和标记清除法的优缺点?

    标记复制法更加耗内存,因为始终有一个幸存区是空着的。由于年轻代对象存活时间短,所以一次GC存活下来的对象就少,复制所带来的开销就很少。

    当有大量可回收对象时,标记清除法执行效率低。

  6. CMS回收过程?

    CMS=Concurrent Mark Sweep并发标记清除
    步骤:
      1、初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
      2、并发标记:从GC Roots直接关联的对象开始遍历整个对象图,这个过程耗时较长,但可以与用户线程并发运行
      3、重新标记:为了修正并发标记期间因用户线程继续运作而导致的变动,耗时比初始标记长,但远比并发标记短
      4、并发清除:清除掉标记阶段判断的已死亡对象,由于不需要移动存活对象,因此可以与用户线程并发运行
    CMS停顿时间短的原因:
      由于整个过程耗时最长的是并发标记阶段和并发清除阶段,但这两个阶段都可以与用户线程并发运行,因此整个过程停顿时间就短
    
  7. volatile解决什么问题?为什么要禁止指令重排序?

    保证可见性和禁止指令重排序。单核CPU即使指令重排序也没关系,但多核CPU的话,指令重排序会导致代码逻辑发送错误。

    指令重排序是一种在CPU层面上为了提高指令执行速度而采取的一种手段。

  8. volatile实现原理?什么是可见性?

    加了volatile的变量,在编译后会在该变量的前面加上lock前缀指令,lock前缀指令相等于内存屏障的作用,该变量前的指令一定在该变量前执行,该变量后的指令一定在该变量后执行。

  9. JUC下面有啥?

    原子类、并发队列、并发集合、AQS、锁、线程工具类

  10. 类加载机制?

    数组没有对应的字节流,由Java虚拟机直接生成。其它类则由类加载器进行加载。
    类加载阶段:
      加载:查找字节流,并生成Class对象(堆区)
      连接:
        验证:确保字节流不会危害虚拟机安全
        准备:为静态域分配空间,并填充0
        解析
      初始化:初始化静态域
      使用
      卸载
    双亲委派的好处:
      1、不管是哪个加载器加载的,最终都是委托给父类加载器去加载,只有父类加载器加载不了,才交由子类加载器去加载
      2、可以避免重复加载
    打破双亲委派规则(覆写loadClass方法)可创建多个Class对象
    
  11. 什么情况下类会被卸载?

    当满足以下条件,类可以被卸载:
      1、该类所有的实例已经被回收
      2、加载该类的ClassLoder已经被回收
      3、该类对应的java.lang.Class对象没有任何对方被引用
    JVM自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载
    通过虚拟机参数,可以查看类的加载与卸载过程:
      -verbose:class 跟踪类的加载和卸载
      -XX:+TraceClassLoading 跟踪类的加载
      -XX:+TraceClassUnloading 跟踪类的卸载
    通过System.gc()来促进回收
    
  12. 如何判断对象可回收?

    引用计数法(循环引用无法判断)

    可达性分析法(如果某个对象可以与GC Roots对象产生直接或间接关联)

  13. JVM工具有哪些?

    jps(查看Java进程)、jstat(监控状态)、jmap(生成堆快照)、jstack(生成线程快照)

  14. 什么是强软虚弱引用?

    强引用就是平常用的引用。

    软引用SoftReference,当内存不够用了会被回收

    弱引用WeakReference,内存就算够也会被回收

  15. JVM内存分配规则?

    优先在Eden区,大对象直接进入老年代,长期存活对象进入老年代,空间分配担保(Minor GC存活下来的对象幸存区放不下直接进入老年代)

  16. 为什么年轻代对象年龄超过15进入老年代?

    因为对象头只用四个bit位存年龄,最大可表示15。

  17. FullGC频繁如何排查?

    https://www.jianshu/p/e749782fff2b

  18. 如果线上项目特别慢,如何排查?

    从具体的某一个请求去分析,看看是前端代码问题,还是网络问题,还是后端问题。
    如果是后端问题,看看是服务内部出了问题,还是访问服务外部组件出了问题。
    服务内部主要是分析日志,通过日志分析有没有出现线程卡住的情况,如果有则通过jstack,arthas等工具分析是否出现死锁、阻塞等情况
    还可以通过GC日志分析是否频繁Full GC导致用户线程停顿,如果是,则可dump出堆内存快照文件,然后用MAT或JProfiler工具分析是否有大对象或某一类型的对象特别多,从而定位出有问题的代码
    如果是访问外部组件有问题比如数据库,可通过数据库连接池的监控页面比如druid或慢查询日志等分析sql的查询时长,如果有查询比较慢的sql,可通过执行计划查询索引是否失效,另外可针对此sql做一些优化
    
  19. long和double非非原子性协定?

    Java内存模型要求lock、unlock、read、load、assign、use、store、write这8个操作都具有原子性,但是对于64位的数据类型(double、long)定义了相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次的32位操作来进行,即允许虚拟机可以不保证64位数据类型的load、store、read和write操作的原子性。如果一个线程正在修改该 long 变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile 型的 long 或 double 变量的读写是原子的。

  20. 内存屏障?

    volatile 提供 happens-before 的保证。volatile 修饰符的另一个作用是提供内存屏障。通过读屏障和写屏障来保证指令的有序性。

  21. byte b = 130;有没有问题?如果我想让赋值正确,可以怎么做?结果是多少呢?

    肯定是报错的,因为130是int类型,将int赋给byte需要强转。
    130是int类型,那么赋值给byte类型,只保留最后一个字节,其二进制是1000 0010
    而计算机中数据都是以补码表示的,正整数的原码、反码、补码完全一样,即符号位为0,数值位相同。而负数的补码=反码+1,其中符号位始终为1且不参与运算。
    因此对于1000 0010来说,其原码的十进制为-126,因此b为-126。

  22. System.out.println((int)(char)(byte)-1)结果是多少?

    1、-1是高精度的int类型转换为低精度的byte类型需要进行强制转换结果还是-1
    2、char类型占用内存的字节数为两个字节,一个字节8位,所以char能够存储的取值范围位0~65535;然而-1是不在这个取值范围内的,所以会倒着查找,因此转换为char类型时值为65535
    3、char类型的65535强制转换为int类型时值还是65535

  23. 线程中断相关方法

    当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态).在Java中提供了以下三个与中断相关的方法:
    //中断线程(实例方法)
    public void Thread.interrupt();
    //判断线程是否被中断(实例方法)
    public boolean Thread.isInterrupted();
    //判断是否被中断并清除当前中断状态
    public static boolean Thread.interrupted();

  24. 说说你知道的几种主要的JVM参数
    思路: 可以说一下堆栈配置相关的,垃圾收集器相关的,还有一下辅助信息相关的。

    1、堆栈配置相关
    java -Xmx3550m -Xms3550m -Xmn2g -Xss128k 
    -XX:MaxPermSize=16m -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=0
    -Xmx3550m: 最大堆大小为3550m。
    -Xms3550m: 设置初始堆大小为3550m。
    -Xmn2g: 设置年轻代大小为2g。
    -Xss128k: 每个线程的堆栈大小为128k。
    -XX:MaxPermSize: 设置持久代大小为16m
    -XX:NewRatio=4: 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。
    -XX:SurvivorRatio=4: 设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6
    -XX:MaxTenuringThreshold=0: 设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。
    2、垃圾收集器相关
    -XX:+UseParallelGC
    -XX:ParallelGCThreads=20
    -XX:+UseConcMarkSweepGC 
    -XX:CMSFullGCsBeforeCompaction=5
    -XX:+UseCMSCompactAtFullCollection:
    -XX:+UseParallelGC: 选择垃圾收集器为并行收集器。
    -XX:ParallelGCThreads=20: 配置并行收集器的线程数
    -XX:+UseConcMarkSweepGC: 设置年老代为并发收集。
    -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生碎片,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
    -XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片
    3、辅助信息相关
    -XX:+PrintGC
    -XX:+PrintGCDetails
    -XX:+PrintGC 输出形式:
    GC 118250K->113543K(130112K), 0.0094143 secs
    -XX:+PrintGCDetails 输出形式:
    GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs[Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs
    
  25. 除直接调用System.gc外,触发Full GC执行的情况有如下四种。
    (1)老年代空间不足
    老年代空间只有在新生代对象转入及创建大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:java.lang.OutOfMemoryError: Java heap space。
    为避免以上两种状况引起的FullGC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。
    (2)永久代空间满
    永久代中存放的为一些class的信息等,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用CMS GC的情况下会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:java.lang.OutOfMemoryError: PermGen space。
    为避免Perm Gen占满造成Full GC现象,可采用的方法为增大永久代空间或转为使用CMS GC。
    (3)CMS GC时出现promotion failed和concurrent mode failure
    对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。promotionfailed是在进行Minor GC时,survivor space放不下、对象只能放入老年代,而此时老年代也放不下造成的;
    concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的。应对措施为:增大survivorspace、老年代空间或调低触发并发GC的比率。
    (4)统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间
    Hotspot为了避免由于新生代对象晋升到老年代导致老年代空间不足的现象,在进行Minor GC时,做了一个判断,如果之前统计所得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,那么就直接触发Full GC。例如程序第一次触发MinorGC后,有6MB的对象晋升到旧生代,那么当下一次Minor GC发生时,首先检查老年代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。

  26. Java中会存在内存泄漏吗?
    理论上来说,Java是有GC垃圾回收机制的,也就是说,不再被使用的对象,会被GC自动回收掉,自动从内存中清除。但是,即使这样,Java也还是存在着内存泄漏的情况:
    长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露。比如像hibernate的Session(一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭(close)或清空(flush)一级缓存就可能导致内存泄露

  27. 在Java中,子类可以从父类中继承哪些?
    (1)继承public和protected修饰的属性和方法,不管子类和父类是否在同一个包里。
    (2)继成默认权限修饰符修饰的属性和方法,但子类和父类必须在同一个包里。
    (3)无法继承private修饰的属性和方法。
    (4)无法继承父类的构造方法

  28. 以下代码是否有错,有的话怎么改?
    short s1= 1;
    s1 = s1 + 1;
    有错误.short类型在进行运算时会自动提升为int类型,也就是说s1+1的运算结果是int类型,而s1是short类型,此时编译器会报错.

    short s1= 1;
    s1 += 1;
    +=操作符会对右边的表达式结果强转匹配左边的数据类型,所以没错.

  29. volatile原理
    观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
    lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:
    1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2、它会强制将对缓存的修改操作立即写入主存;
    3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
    volatile防止指令重排序的原理就是内存屏障,而这个内存屏障就是CPU的lock前缀指令

  30. happends-before原则
    JSR-133(JDK1.5)内存模型规范使用Happens-before的概念来阐述操作之间的内存可见性。在JVM中如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间必须要存在Happens-Before关系
    八种happends-before原则:
    单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
    锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
    volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
    happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
    线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
    线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
    线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
    对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

  31. TCP粘包与拆包问题解决方案
    1、消息定长,如每个报文大小固定为200字节,如果不够则补空格
    2、包尾增加换行符,如FTP协议
    3、消息分为消息头和消息体,消息头中包含消息总长度
    4、更复杂的应用层协议
    Netty通过LineBasedFrameDecoder解决TCP粘包问题,原理是通过\n或\r\n分隔,Netty服务端和客户端应使用相同的解码器
    Netty提供了多种TCP粘包/拆包的编码器,用来满足用户的不同需求

    Netty支持多种通信协议:Socket、Http、Websocket、UDP、文件传输
    Reactor三种模型:
      Reactor单线程模型
      Reactor多线程模型
      主从Reactor多线程模型
    Netty支持Reactor三种模型
    

2、集合

  1. 线程安全的集合有哪些?

    Vector、CopyOnWriteArrayList
    CopyOnWriteArraySet
    HashTable、Properties、ConcurrentHashMap
    
  2. HashMap和HashTable的区别?

    HashTable是线程安全的。
    HashMap继承自AbstractMap,HashTable继承自Dictionary。
    HashTable不允许key/value为null。
    
  3. HashMap的实现原理,成环问题?

    JDK1.7:数组+链表。JDK1.8:数组+链表+红黑树。

    初始容量16,默认负载因子是0.75,当元素个数超过16*0.75=12时进行二倍扩容。

    JDK1.7在复制单链表时,是在while循环里面,单个计算好数组索引位置后,单个地插入新数组中,在多线程情况下会有成环问题。

    JDK1.8是等链表整个while循环结束后,才给新数组复制,且新、老值的头指针都是局部变量,在多线程下不会成环。

  4. ArrayList初始容量,扩容原理?

    调用ArrayList的空构造方法,elementData则是一个长度为0的空数组。

    第一次添加元素,会扩容,扩容之后,容量为10。

    之后,当向集合中添加元素达到集合的上限(也就是minCapacity大于elementData.length)时,会对集合再次扩容,扩容为原来的1.5倍。

  5. ConcurrentHashMap jdk1.7和1.8的区别?

    JDK1.7:ReentrantLock+Segment+HashEntry,JDK1.8:synchronized+CAS+HashEntry+红黑树

    用 Synchronized + CAS 代替 Segment ,这样锁的粒度更小了(锁住一个槽位,Node对象当锁),并且不是每次都要加锁了,CAS尝试失败了在加锁

    JDK1.7需要两次定位

  6. HashMap什么时候出现线程不安全问题?

    扩容时单链表复制,会导致成环问题。

    并发写时,会发生数据覆盖。

  7. HashMap为什么大小一定是2的n次方?

    因为计算索引是通过&运算,如果不是2的n次方的话,那么length-1的二进制必有一位是0。另外,也方便扩容时对原数组上的元素进行一个定位。

3、多线程

  1. synchronized的底层原理?

    synchronized关键字编译后会在同步块前后分别形成monitorenter和monitorexit两个字节码指令。
    每个Java对象都有一个对象头,而对象头里面有一个指针,指向一个monitor对象,monitor可以理解为对象监视器,monitor对象由C++实现,底层依赖的是操作系统的互斥量(mutex)。
    执行monitorenter指令时,首先尝试获取锁,如果这个对象没有被锁定,则当前线程将持有锁,monitor计数器会加1,否则当前线程阻塞。
    执行monitorexit指令时,monitor计数器会减1,如果减为0则代表锁被释放。
    
  2. synchronized与Lock的区别?

    ReentrantLock是Java代码层面的锁,而synchronized关键字是Java在JVM层面的锁。
    synchronized锁是一个内置关键字,synchronized更加简单;而ReentrantLock是一个Java类,提供了丰富的API,使用起来更加灵活。
    synchronized自动释放锁(代码执行完或发生异常),ReentrantLock需要手动释放锁。
    synchronized是非公平锁,ReentrantLock可公平可不公平。
    synchronized不可中断,ReentrantLock可中断。
    synchronized适用于少量代码的同步问题,ReentrantLock适用于大量代码的同步问题。
    两者都可重入,都是独占锁、悲观锁思想。
    
  3. 线程池submit和execute的区别?

    JDK5往后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,它们的区别是:
    execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
    submit(Runnable|Callable x) 返回一个future。可以用这个future来判断任务是否成功完成。
    如果提交的任务不需要一个结果的话直接用execute()会提升很多性能。
    
  4. 线程池有哪些参数?

    corePoolSize:线程池中的核心线程数
    maximumPoolSize:线程池中的最大线程数
    keepAliveTime:线程池中当前线程数超过corePoolSize时,多余的空闲线程的存活时间,即多少时间内会被销毁(核心线程不会被销毁)
    unit:keepAliveTime的单位
    workQueue:任务队列,已提交但尚未被执行的任务
    threadFactory:线程工厂,用于创建线程
    handler:拒绝策略,当任务太多来不及处理(即任务队列已满时),如何拒绝任务
    
  5. 线程池的好处?

    1、当有大量异步任务时,如果来一个任务,就new一个线程去执行,线程的创建和销毁是需要开销的。
    线程池里面的线程是可复用的。
    2、如果来一个任务就new一个线程,那么大量的线程会产生高昂的线程上下文切换开销。
    3、线程池提供了一种资源限制和管理的手段,比如可以限制线程的个数。
    
  6. 线程池执行原理

    1.如果count<coreSize,则调用addWorker(任务,是核心线程)开启一个核心线程运行任务;
    2.如果count>=coreSize,先不着急开启非核心线程运行任务,而是添加到阻塞队列中;
    3.如果阻塞队列满了,但count<maxSize,就调用addWorker(任务,非核心线程)开启一个非核心线程运行任务;
    4.如果阻塞队列满了,并且count>=maxSize,就直接拒绝
    
  7. 线程池线程复用原理?

    通过无限循环从队列中拉取任务,如果获取任务超过了一定时间还未获取到(这段时间即该Worker的空闲时间,如果超过了起初设定的空闲时间,该Worker是要被回收的)
    除非设置核心线程也能被回收,否则核心线程就算拉取任务超时了,也会不停地重试
    
  8. 有哪些线程安全且有界的阻塞队列?

    ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedBlockingQueue(可有界可无界)

  9. 怎么处理并发问题?

    CAS、synchronized、Lock、并发集合、并发队列、线程池、线程工具类、基于数据库的乐观锁、通过缓存、通过消息队列削峰

  10. 线程的原理和实现?

    操作系统的原生线程,抢占式。

  11. AQS原理?

    AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch等。
    AQS维护了一个等待队列,等待队列是CLH(Craig Landin Hagersten)锁双向队列,多线程争用资源被阻塞时会进入此队列。
    AQS用了Node head和Node tail两个指针来维护队列。
    AQS维护了一个volatile int state(代表共享资源),AQS只是定义了state,但state具体的含义由同步器子类定义,比如CountDownLatch的初始化state为线程数,ReentrantLock的初始化state为0,拿到锁后加1,每重入一次也加1。
    AQS也定义了独占和共享两种资源访问方式。
    自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
    自定义同步器实现时主要实现以下几种方法:
      isHeldExclusively():该线程是否正在独占资源,只有用到condition才需要去实现它;
      tryAcquire(int):独占方式,尝试获取资源;
      tryRelease(int):独占方式,试释放资源;
      tryAcquireShared(int):共享方式,尝试获取资源;
      tryReleaseShared(int):共享方式,尝试释放资源;
    
  12. 锁优化和锁升级(JDK1.6之后)?

    锁优化:
      自适应自旋:自旋还包括普通自旋和固定次数的自旋
      锁消除:编译时检测到不可能被共享访问
      锁粗化:由循环里面加锁改为循环外面加锁
      轻量级锁:如果没有竞争则通过使用CAS避免互斥量的开销,否则必须膨胀为重量级锁
      偏向锁:偏向于第一个使用它的线程,如果一直只有这个线程访问,则直接消除同步
    
  13. CAS以及问题?

    内存值V、预期值A、更新值B。每次执行操作前判断A与B是否相等,如果相等,则将内存值V改为B。先检查后操作必须是原子性的(通过CPU的一个原子性指令来保证)。
    问题:
      自旋消耗CPU
      ABA问题:另一个线程将A改为B,又将B改为A。用基于时间戳的AtomicStampedReference来解决。
      只能保证一个变量的原子操作
    
  14. ThreadLocal原理?

    ThreadLocal其实只是一个空壳,而所谓的线程本地变量其实是存储在Thread类的threadLocals变量里的;
    threadLocals是ThreadLocal.ThreadLocalMap类型,ThreadLocal::set调用ThreadLocalMap::set将线程本地变量放置到threadLocals中;
    Thread的threadLocals变量初始值是null,当第一次设置值的时候才会实例化;
    ThreadLocalMap是一个定制化的Map,采用线性探测法。
    至于如何让threadLocals只能存储在线程本地而不共享就可能与C或C++有关了。
    ThreadLocal有一个问题就是,子线程无法获取父线程的ThreadLocal变量,可用InheritableThreadLocal解决。
    
  15. 线程池有哪些状态?

    1、RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
    2.SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。调用线程池的shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
    3.STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。调用线程池的shutdownNow()方法时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
    4.TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。因为terminated()在ThreadPoolExecutor类中是空的,所以用户想在线程池变为TIDYING时进行相应的处理;可以通过重载terminated()函数来实现。 
    
    当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
    当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
    
    5.TERMINATED:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED
    
  16. 什么是fork join?

    Fork Join 并发任务执行框架。
    Fork Join 体现了分而治之。什么是分而治之?规模为N的问题,如果N<阈值,直接解决,N>阈值,将N分解为K个小规模子问题,子问题互相对立,与原问题形式相同,将子问题的解合并得到原问题的解。

    Fork Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干了小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join汇总

4、Redis

  1. 缓存雪崩、击穿、穿透

    缓存雪崩解决办法:key过期时间分散开;通过加锁、队列或限流的方式保证不会有大量线程在同一时间进行读写。
    缓存穿透解决办法:布隆过滤器;不存在的值也缓存起来,但过期时间设短一点。
    缓存击穿:定时任务主动刷新缓存;加锁(缓存有数据,返回;没数据,尝试拿锁;拿到锁,查数据库并同步到缓存;没拿到锁,再查一次缓存,如果缓存为没数据则重试,有数据则返回)
    
  2. redis主从脑裂问题,如何解决?

    对于redis主从架构,master接受到请求之后执行完会立刻返回给client,然后会异步复制给其他master,此时会出现两种问题:
      1、当集群节点间网络或其他问题导致异步复制延时很高,如果此时master宕机了,毫无疑问会丢失延时的这段时间的数据。
      2、当网络分区变化导致master和slave节点之间无法正常通信时,sentinel哨兵集群会选举slave为master,此时与之前master连接的client一直写数据,当我们进行恢复将原master当做新master的slave节点的时候,那么client后来写到原master内存的数据会丢失(脑裂)
    
    解决上述两种数据丢失的问题,redis配置文件中有以下两行:
      min-slaves-to-write 3
      min-slaves-max-lag 10
    意味着至少要有3个slave节点与master保持10秒钟以内的数据同步,否则master就不会接受新的请求,我们需要采取其他措施来应对。
    
  3. Redis哈希槽的概念,以及新增节点后如何迁移数据?

    默认会分配16384个插槽。将键名的有效部分使用CRC16算法计算出散列值,然后对16384取余。

    新增的节点,要么以从库身份复制主库数据,要么以主库身份获取哈希槽分配权。

    假如现在有ABC三个主节点,新增一个主节点D,只需要将ABC三个节点的部分槽迁移到D即可。删除节点同理。

    增删节点或是修改某个节点的槽数量都不会导致集群不可用。

    cluster addslots命令 分配插槽,cluster delslots命令 删除插槽

    Redis集群不保证强一致性,主要原因有:主从是异步复制的;形成网络分区(脑裂)

  4. 基于Redis的分布式锁?

    public String deductStock() {
        String lockKey = "product_001";
        try {
           /*Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa"); //jedis.setnx
            stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); //设置超时*/
            //为解决原子性问题将设置锁和设置超时时间合并
            Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa", 10, TimeUnit.SECONDS);
     
            //未设置成功,当前key已经存在了,直接返回错误
            if (!result) {
                return "error_code";
            }
            //业务逻辑实现,扣减库存
            ....
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }
    

    当前锁的失效时间为10s,如果当前扣减库存的业务逻辑执行需要15s时,高并发时会出现问题:线程1,首先执行到10s后,锁已过期,但业务还没执行完,线程2却可以获取锁。

    解决方案:定义一个子线程,定时去查看是否存在主线程的持有当前锁,如果存在则为其延长过期时间

  5. 缓存和数据库一致性问题解决方案?

    https://blog.csdn/xl_1803/article/details/120471636

  6. redis重分片实现原理?

    先迁移数据,再迁移槽。如果数据还没迁移走,那么在源节点上执行命令是OK的;相反,如果数据迁移走了,此时客户端还去源节点执行命令的话,会判断集群是否正在迁移key所属的槽,如果没有,则返回key不存在,如果有,则向客户端返回ASK错误。(集群版的而非单机版的)客户端收到ASK错误后会自动根据错误提供的IP和端口号进行转向动作,从而在目标节点上执行命令。如果想看到ASK错误的话,可以使用单机版的redis-cli客户端。

  7. MOVED错误和ASK错误的区别?

    MOVED错误表示槽的负责权已经从一个节点转移到了另一个节点

  8. redis大key热key解决方案?

    https://wwwblogs/cxy2020/p/13810963.html

    对于大key,可以对元素按范围(可对每个元素加一个序号标识)、或hash后取模进行分片,分片后可以放在同一个节点,也可以放在不同节点。

    而热key拆分后是必须要放在不同的redis节点的。

    已知条件是节点个数和各结点的ip:port,分片时field所在节点计算方式为field.hash%count,如果要求元素有序,则需要field包含一个单挑递增的序号。

  9. Redis单线程模型?

    因为文件事件处理器是单线程的,因此称Redis为单线程的。

    Redis初始化的时候,会将事件类型与具体的事件处理器关联起来。

  10. Redis压缩链表

    zlbytes(4字节,记录压缩链表总字节数)

    zltail(4字节,记录尾结点距起始地址的字节数,这样就不用遍历整个链表就能快速定位到尾结点了)

    zllen(2字节,节点数量)

    节点(每个节点大小不确定,由节点内容确定。这正是压缩链表节省内存的原因。每个节点中记录了上个节点的长度,因此能够从尾结点向前遍历)

    zlend(1字节,值是255,用于标记压缩链表末端)

    节点的属性:

    ​ previous_entry_length:记录上一个节点的长度(如果小于254个字节,则该属性占1字节;否则占5字节)

    ​ encoding:记录节点数据的类型与长度

    ​ content:可以是一个字节数组或一个整数

    连锁更新问题:假设n个节点,长度都是253个字节,那么每个结点的previous_entry_length都占1字节,此时在n个节点的最前面插入一个新节点,占254个字节,那么第二个节点需要将previous_entry_length属性从1字节调成5字节,这还没完,关键是该节点增大了4个字节,超过254了,那后面的节点都要将自己的previous_entry_length属性从1字节调成5字节,这就是连锁更新问题。对应地,删除节点也会发生连锁更新问题。redis没有解决连锁问题,因为发生这种问题的概率是很低的。

  11. Redis整数集合

    encoding:可以保存int16_t、int32_t、int64_t的整数值,目的的为了节省内存,会有升级操作(比如将int[]变成long[]),但不支持降级

    length:元素数量

    contents:数组,从小到大排序

  12. Redis字典与哈希表

    字典由两个哈希表组成,一般是使用第0个hash表,但在rehash时,两个hash表都会使用,还会记录rehash的进度。

    Redis使用的Hash算法是:MurmurHash2,求余用的也是&,解决哈希冲突用的是拉链法。

    如果是扩容,则ht[1].len=第一个大于等于ht[0].used2的2的n次方

    如果是缩容,则ht[1].len=第一个大于等于ht[0].used的2的n次方

    动态负载因子=ht[0].used / ht[0].size

    扩容条件:没有执行bgsave或bgrewriteaof时,要求负载因子大于等于1,否则要求大于等于5(因为这两个命令会fork出子进程,需要在子进程存在期间避免对内存写入从而节约内存)

    缩容条件:负载因此小于0.1

    渐进式rehash:目的是避免大量的数据需要做rehash时可能会导致redis服务不可用

    rehash期间的四种操作:

    查、删、改:先在ht[0]上找,找不到再在ht[1]上找

    增:只会在ht[1]上增,以此让ht[0]只减不增

    rehashindex初始值为0,每执行一次操作,就将当前rehashindex上的元素rehash掉,然后rehashindex++

  13. redis跳表

    redis跳表源码如下。

    typedef struct zskiplistNode {
        sds ele; // 元素值
        double score; // 元素分数
        struct zskiplistNode *backward; // 后退指针,支持从后向前遍历(不支持索引)
        // 层级数组
        struct zskiplistLevel {
            // 前进指针
            struct zskiplistNode *forward;
            // 向下一步前进时的跨度,记录这个的目的是快速计算Rank(排名)
            unsigned long span;
        } level[];
    } zskiplistNode;
    
    typedef struct zskiplist {
        struct zskiplistNode *header, *tail; // 指向跳跃表的表头和表节点
        unsigned long length; // 跳跃表节点个数,不包括表头节点。可以在O(1)时间内返回跳表长度
        int level; // 目前的跳跃表,层数最大的那个节点的层数。计算时不包括表头节点
    } zskiplist;
    // zset实际是由字典和跳表共同组成的。字典的目的是能在O(1)时间内根据元素获取它的分值,而跳表是可以做范围查询的
    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    跳表如下图所示。

    redis的跳跃表并不是一个理想的跳跃表(每两个元素抽出一个索引),因为理想的跳表在增删元素时难以维护。

    新增一个跳表节点时,根据幂次定律(越大的数出现的概率越小),随机生成1-32之间的数,作为该节点的层级数(即level数组的大小),假设得到的数是5,则。建立最底层的5级索引

    删除元素就比较简单了,找到各级元素中包含要删除元素的节点,使用链表的方式删除即可。

  14. redis缓存击穿或缓存雪崩时,如果通过加锁的方式解决,高并发场景下会造成过多的线程阻塞,如何解决?

    缓存预热,避免第一次请求数据库

    将过期时间分散开

    搭建高可用的主从模式,避免单点故障

    使用Ehcache本地缓存,在redis集群故障时,可以抵挡一阵

    利用限流组件如Hystrix做限流,做缓存降级,如返回服务繁忙等友好提示信息,或返回默认值

  15. Key竞争问题

    多客户端并发型的写一个key,本来按照顺序修改为4,3,2,但是由于并发的原因,导致顺序为4,2,3最后的key值变成3了,这种现象称为竞争key

    解决方法:分布式锁或消息队列

5、消息队列

  1. RabbitMQ和Kafka的区别?

    应该场景:RabbitMQ用于实时的,对可靠性要求较高的消息传递上。kafka用于处于活跃的流式数据,大数据量的数据处理上。
    开发语言:分别是erlang和scala。
    单机吞吐量:RabbitMQ是万级,而Kafka是10万级。
    速度:RabbitMQ是微秒级,Kafka是毫秒级。
    可用性:RabbitMQ是主从架构,Kafka是分布式架构。
    功能特性:RabbitMQ基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富;Kafka只支持主要的MQ功能,像一些消息查询、消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。
    
  2. kafka读写速度快的原因?

    kafka是顺序写入。

    利用分区实现并行处理。

    Zero Copy

    批处理:读写

    数据压缩

  3. Kafka中是怎么体现消息顺序性的?

    kafka每个partition中的消息在写入时都是有序的,消费时,每个partition只能被每一个group中的一个消费者消费,保证了消费时也是有序的。
    整个topic不保证有序。如果为了保证topic整个有序,那么将partition调整为1。

  4. 消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?

    offset+1

  5. kafka消息丢失解决方案?

    生产者:

    ​ 同步模式下,需要设置为-1 producer.type = sync request.required.acks=-1

    ​ 异步模式,不能设置为0(缓冲区满了就丢弃消息),而应不限制阻塞超时时间,满了的话阻塞生产者。

    消费者:

    ​ 如果是自动提交,那么还没来得及消费就挂了的话,就会丢失消息。因此需要使用kafka高级API并设置为手动提交。

  6. kafka重复消费解决方案?

    消费者:

    ​ 批量消费500条时,已经消费了200条,但没来得及提交偏移量进程就被强行kill掉。这样会导致200条数据会被重复消费。通过外部去重介质,如Mysql唯一索引去重。

6、Mysql

  1. SQL过慢,怎么排查,怎么优化?

    1、肉眼排查:select *、子查询过多、在表连接的列上加索引
    2、explain执行计划,看是否有没用上的索引
    3、结合业务优化SQL
    4、查看数据库服务器CPU和内存使用率
    5、优化表结构:适当增加冗余字段
    6、调整数据库服务器参数
    
    
    工作中做过一些优化,首先并不是出了问题才优化,在进行数据库建模和数据库设计时会预先考虑一些问题,比如表字段的类型、长度,以及创建合适的索引;
    当生产环境出了问题后,会通过一些监控工具、慢查询日志、执行计划、索引以及架构等方面综合考虑,然后再做出调整,比如调整sql语句,调整索引,使用分库分表,调整数据库参数等;
    最后举例;
    
  2. 执行计划怎么看?

    select_type:查询类型
      simple:简单查询,不包括连接查询和子查询
      primary:主要的查询,最外层的查询
      subquery:子查询
    table:表示查询的表
    type:表的连接类型
      const:只返回一行数据,查询速度很快,该列具有唯一索引或主键索引;如果是普通索引,即时真的只返回一行数据,也不会是const;
      range:范围查询;
      index:根据辅助索引全表扫描;
      ALL:无索引(聚集索引)全表扫描;
    key:实际使用的索引;
    
    比较重要的key、extra、type:
    	1、type:system>const>eq_ref>ref>fulltext>ref_or_null>index_merge>unique_subquery>index_subquery>range>index>ALL,一般来说,好的sql查询至少达到range级别,最好能达到ref
    	2、extra:
    		Using filesort:mysql对数据使用一个外部的索引排序,而不是按照表内的索引进行排序读取。也就是说mysql无法利用索引完成的排序操作成为“文件排序”
    		Using index:使用了覆盖索引;如果同时出现Using where表明索引被用来执行索引键值的查找,否则表明索引用来读取数据而非执行查找动作
    		Using where:使用了where过滤
    		Using join buffer:使用了连接缓存
    		Using index condition
    
  3. sql优化技巧?

    索引并非越多越好,索引太多会影响insert、update、delete的性能;
    经常更新的列不使用索引,经常查询(逻辑外键、where子句、group by子句、order by子句)的列使用索引;
    数据量小的表不使用索引(区分度);
    重复值很多的列不使用索引;
    删除长期未使用的索引,不用的索引会造成不必要的性能消耗;
    在外键上建立索引加快表连接的速度;
    select *
    优化子查询(会生成中间表)
    遵循最左前缀原则
    
  4. Mysql用自增主键好还是用别的方式生成ID?性能方面有什么差异?

    用自增ID做主键,数据是有序的,按照插入顺序,如果用UUID,新行的值不一定要比之前的主键的值要大,所以 innodb 无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。因为写入是乱序的,innodb 不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据。

    性能方面,如果用UUID做主键,占用空间大,每页能存放的索引少,这会导致磁盘IO次数增加,效率变慢。

  5. 什么是覆盖索引?

    首先有两个概念:聚集(主键)索引、辅助(非聚集、二级)索引

    聚集索引的叶子结点存放一整行数据,而辅助索引的叶子结点只存放该列数据和主键;

    每张表只能有一个聚集索引,如果是select *操作,则优化器更倾向于使用聚集索引,如果使用辅助索引则需要回表查询,增加了IO次数降低效率

    如果能直接从辅助索引上查找到所有想要的数据,而不需要回表,就称该辅助索引覆盖了这条select语句,又称索引覆盖

  6. 悲观锁和乐观锁怎么用SQL语句实现?

    for update 悲观锁

    lock in share 乐观锁

  7. Mysql间隙锁?

    间隙锁锁定一个范围,不包含本身。next-key locking=间隙锁+本身,mysql用next-key locking解决幻读问题(侧重数据的插入和删除)。

    比如select count(*) from tab where b>3,必须要锁住3到正无穷这个区间,否则会出现幻读。

  8. 事务的隔离级别和并发问题?

    隔离级别:读未提交 读已提交(MVCC当前读) 可重复读(MVCC快照读+next-key loking) 序列化度

    并发问题: 脏读 不可重复度+幻读 第二类丢失更新

  9. select语句的执行顺序?

    from where group by having select order by

  10. 复合索引AC会走索引吗?

    会,AC实际上走的是索引A。除了AC,还有A、AB、ABC会走索引。

    优化器会自动调整and前后的顺序。

    遇到范围查询(>、<、between、like)就会停止匹配。

  11. 基于版本号机制的乐观锁?

    数据库版本号乐观锁机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

  12. 索引失效的情况?

    where子句不要对字段施加函数

    in不走索引,可用exists代替,如果是连续的值可用between代替

    避免使用or连接查询条件,一个字段有索引,一个字段没索引,则不走索引

    like不能前置百分比

    where子句中不要用!=或<>

  13. Mysql二进制日志和重做日志区别?

    重做日志只记录InnoDB事务日志。一次事务写一次二进制日志,而重做日志在事务进行过程中不断有重做日志条目写入。

  14. InnoDB行锁?共享锁S和排它锁X的兼容性?

    只有S和S能兼容。另外不带lock in share的普通select语句与S锁和X锁都能兼容,因为它走的是快照读。

  15. 什么是InnoDB一致性非锁定读?

    通过MVCC机制,有快照读和当前读,快照读就是一致性非锁定读。

  16. 如何创建索引?

    可视化界面或用create index语句

  17. InnoDB和MyISAM索引区别?

    最大的区别就算InnoDB支持事务。

  18. SQL索引失效?有OR条件的话一定会失效么?

    不一定。如果OR连接的两个条件都有索引,则不会失效。因为如果一个有索引,一个没有索引,有索引的会走索引,没有索引的会查全表,那还不如直接查一次全表呢,用了索引还多了一次走索引的时间。

  19. 预编译原理?

  20. count(*) count(列名) count(1)的区别?

    MyISAM存储引擎会存储总行数,因此如果没有where条件,则MyISAM引擎会瞬间返回行数。
    count(*)统计行数时包括所有列,不会忽略NULL
    count(1)统计行数时忽略所有列,因此也不会忽略值NULL
    count(列名)只统计某一列的行数,会忽略NULL
    对于InnoDB:
      当列名为主键,count(列名)>count(1),否则相反
      当表有多个列且没有主键,count(1)>count(*),如果有主键,则count(主键)是最快的
      当表只有一个字段,则count(*)最快
    
  21. 数据库优化方案?

    尽量用固长字段,或可变长较小(如varchar(10))的字段

  22. 深度分页如何优化?

    select * from stu where age>10 limit 890000,10  //3s
    直接走聚集索引做全表扫描然后进行分页
    select a.* from stu a right join (select id from stu where age>10 limit 890000,10) b on a.id=b.id //0.5s
    利用覆盖索引数据量小的特点,在覆盖索引上快速分页,然后再回表查询
    
  23. update一定会锁住整行吗?

    update只有在有索引的情况下会使用行锁,否则锁住整个表。

  24. 为什么索引使用B+树而不是B树
    B+树没有回旋查找问题。B+树能存储的数据更多,因为非叶子结点都是索引,假设范围是1到10,只需要非叶子结点提供两个指针分别指向1和10即可,而B树则需要10个指针。一般3到4层就能存储千万数据。

  25. 用int还是varchar作为索引的key?
    非叶子节点只存储key,而叶子节点存储key和data。key越小,能存储的数据更多。当varchar小于4个字节时,用varchar好,否则用int好。

  26. 什么是索引下推?

    索引下推(index condition pushdown )简称ICP,在Mysql5.6的版本上推出,用于优化查询。

    在不使用ICP的情况下,在使用非主键索引(又叫普通索引或者二级索引)进行查询时,存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件 。

    在使用ICP的情况下,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器 。

    索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。引下推在非主键索引上的优化,可以有效减少回表的次数,大大提升了查询的效率 。

    比如复合索引(name,age),where条件为name=“zhangsan” and age>10,如果没有索引下推,则存储引擎会忽略age,将name="zhangsan"的数据查出来返回给mysql server,将查出来的id做回表再一次次地比对age。如果有索引下推,则不会忽略age,存储引擎同时比对name和age并将结果返回给mysql server。

  27. 前缀索引和索引选择性?

    有时候需要索引很长的字符列(比如BLOB,TEXT,或者很长的VARCHAR类型 ),这会让索引变得大且慢。通常可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。索引的选择性是指不重复的索引值数(也称为基数,cardinality)和数据表的记录总数的比值。索引的选择性越高则查询效率越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的行。唯一索引的选择性是1,这是性能最好的索引选择性。诀窍在于要选择足够长的前缀以保证较高的选择性,同时又不能太长(以便节约空间)。前缀应该足够长,以使得前缀索引的选择性接近于索引的整个列。换句话说,前缀的基数应该接近于完整的列的基数。select count(*) as cnt,left(city,3) as pref from city group by pref order by cnt desc limit 10;

  28. InnoDB四大特性?
    插入缓冲、自适应哈希索引、预读、二次写 https://wwwblogs/zhs0/p/10528520.html

  29. InnoDB版本与Mysql版本的关系?

    Mysql5.5版本中的InnoDB版本为1.1.x,5.6升级到了1.2.x

  30. InnoDB架构?

    内存池=n个内存块
    		缓冲池:读取数据库中的页,并放到缓冲池中(下一次读取同样的页,则直接从缓冲池读)。修改页时,先修改缓冲池,再异步刷新到磁盘(checkpoint)。InnoDB1.0.x版本开始允许有多个缓冲池实例,页平均分配到不同的缓冲池中。
    			数据页
    			索引页
    			undo页
    			其它:插入缓冲、自适应哈希索引、锁信息、数据字典信息
    		LRUList:用LRU算法管理缓冲池中的页,引入了midpoint,即新读取的页并不是放在首部,而是放在midpont,避免全表扫描时读取大量页导致热点页都被淘汰
    		FreeList:存放的是空闲页面,初始化(数据库启动)时申请一定数量的页面。load页面到内存时,会判断FreeList页面是否够用,如果不够用就flush LRUList和FlushList释放空闲页,否则就从FreeList中删除对应的页面,在LRUList中增加页面,保持总数不变
    		FlushList:放脏页,脏页同样也会在LRUList放一份
    	重做日志缓冲:存放重做日志信息,然后以一定频率刷到磁盘。该区域不需要设置很大,有三种情况会触发刷到磁盘:Master Thread每一秒刷一次;事务提交时刷一次;不足1/2刷一次
    	n个后台线程
    		Master Thread:将缓冲池的数据(脏页、合并插入缓冲、undo页的回收)异步刷新到磁盘,保证数据一致性。
    		IO Thread:4 read、4 write、1 insert buffer、1 log IO Thread
    		Purge Thread:事务提交后,undolog不再需要,使用该线程回收已经分配并使用的undo页
    		Page Cleaner Thread:刷新脏页
    
  31. InnoDB逻辑存储结构?

    表空间文件:
    		共享表空间,通过参数innodb_data_file_path设置
    		独立表空间:可以为每个表设置一个独立的表空间,但仅存储该表的数据、索引和插入缓冲bitmap等信息,其余信息仍然放在共享表空间中
    	段:
    		数据段:存放叶子结点
    		索引段:存放非叶子结点
    		回滚段
    	区:区大小是1MB,页大小是16KB,因此一个区中有64个页。顺序预读:将下一个extent预读到缓冲池中
    	页
    	行
    
  32. 什么时候产生当前读,什么时候产生快照读?

    当前读:
    select … lock in share mode
    select … for update
    update、insert、delete
    快照读:
    select

  33. MVCC实现原理?

    隐藏字段:
    	DB_TRX_ID 创建或最后一次修改该记录的事务ID
    	DB_TRX_ID 隐藏主键
    	DB_ROLL_PTR 回滚指针,指向上一个版本	
    	
    readview:事务在快照读时产生的读视图
    	trx_list 系统活跃的事务id
    	up_limit_id 列表中事务id最小的id
    	low_limit_id 系统尚未分配的下一个事务id
    	可见性规则(可视性算法):
    		1、首先比较DB_TRX_ID<up_limit_id,如果小于,则当前事务能看到DB_TRX_ID所在的记录,否则进入下一个判断
    		2、判断DB_TRX_ID>=low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录是在Read View生成后才出现的,那么对于当前事务肯定不可见,如果小于,则进入下一步判断
    		3、判断DB_TRX_ID是否在活跃事务中,如果在,则代表在Read View生成时刻,这个事务还在活跃状态,还没有commit,修改的数据当前事务是看不到的,如果不在,则说明这个事务在Read View生成之前就已经commit了,那么修改的结果是能看见的
    		
    	RC和RR的区别是,Read View生成时机不同,RC是每次查询都生成一次新的Read View,而RR只在第一次查询时才生成Read View	
    
  34. 事务ACID实现原理?

    一致性是数据库最根本的追求,依赖了原子性、隔离性和持久性。
    原子性:undo log
    隔离性:MVCC
    持久性:redo log
    
  35. RR隔离级别下一定是快照读吗?会发生RR吗(也就是RR有没有解决幻读)?

    RR不一定用的是同一个Read View,如果发生了update操作,则会重新生成Read View。因此快照读和当前读一起使用,就有可能产生幻读。RR并没有解决幻读,而是用间隙锁解决的幻读

  36. 如何查看InnoDB锁信息?

  37. set global innodb_status_output_locks=1

    show engine innodb status\G

  38. 什么是WAL技术和二阶段提交?

    二阶段提交:写入redo的过程分为了prepare和commit称为二阶段提交,是为了保证bin log和redo log的一致性。
    		数据库恢复时会利用这两种文件进行恢复。
    		正常是先写redo log后写bin log,如果redo log有而bin log没有,redo log状态为prepare,则回滚。如果两者都有,redo log状态为prepare,则不用回滚。
    
    预写日志:先写日志,再写数据。
            如果不用WAL,则当修改数据时,先从磁盘读到内存,在内存修改后,再刷到磁盘
    	    因为随机读写的效率要低于顺序读写,为了保证数据一致性,可以先将数据通过顺序读写的方式写到日志文件,然后再将数据写到对应的磁盘文件中。只要日志文件写成功了,那么数据就不会丢失	
    
  39. join的原理?

    实际上是做两次查询,先查驱动表,再去查连接表,一般是将小表作为驱动表,减少外层循环的次数。

    https://blog.csdn/agonie201218/article/details/106993948

7、Spring

  1. 微服务核心组件?

    服务注册:Eureka
    服务调用:Ribbon、Feign
    服务容错:Hystrix
    服务网关:Zuul、Spring Cloud Gateway
    配置中心:Spring Cloud Config、Apollo
    链路跟踪
    消息总线
    
  2. Spring MVC有哪些组件?

    前端控制器DispatherServlet
    处理器映射器HandlerMapping
    处理器适配器HandlerAdapter
    处理器Handler
    视图解析器ViewResolver
    
  3. IOC和AOP的好处?

    IOC:
      耦合度低,侵入性低。提供一种构造对象的方式,这样new对象的代码不用散落在各个地方,一些复杂对象的创建也直接交给spring去做。
    AOP:
      AOP将多个类的公共行为封装到一个可重用模块,并将其命名为Aspect。
      AOP把软件系统分为两个部分:核心关注点和横切关注点,业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。
      AOP的使用,使开发人员在编写业务逻辑时可以专心于核心业务,而不用过多的关注于其他业务逻辑的实现,这不但提高了开发效率,而且增强了代码的可维护性 。
      目前最流行的AOP框架有两个,分别为Spring AOP和AspectJ,Spring AOP引入了对AspectJ的支持。
    
  4. SpringBoot特性有哪些?

    默认配置、自动配置

    第三方框架整合

    一般都使用SpringBoot框架来搭建Spring Cloud项目

  5. service层需不需要写接口,如果不写接口有什么影响?Spring AOP的底层原理是什么?

    如果写了接口,默认使用JDK动态代码,否则使用的是cglib代理。

  6. BeanFactory和ApplicationContext的区别

    ApplicationContext是BeanFactory的子类,BeanFactory只提供了最基本的功能,ApplicationContext提供了特定于企业的功能,比如:统一资源访问、AOP拦截、国际化等。

  7. Eureka自我保护机制?

    当开启自我保护机制之后,eureka server会统计15分钟内心跳比例是否低于85%,如果是则开启自我保护机制,让实例不会过期。

    也就是说,从100%到85%,实例会正常移除,而低于85%,则实例不会被移除。当心跳恢复正常后,退出自我保护机制。

    如果实例真的过期了,那么服务消费者会拿到一个无效的地址,此时要做容错机制。

  8. AOP应用场景和通知类型?

    事务、日志、缓存、权限

    前置通知、后置通知、返回通知(在目标方法返回后执行,执行成功之后)、异常通知(在目标方法抛异常时执行)、环绕通知

  9. SpringBoot启动类注解?

    @SpringBootApplication注解里面有一个@EnableAutoConfiguration,@EnableAutoConfiguration里面又有一个@Import(AutoConfigurationImportSelector.class),AutoConfigurationImportSelector类是实现自动配置的关键,它继承自ImportSelector并重写了selectImports方法,该方法的核心逻辑是加载classpath:META-INF/spring.factories文件,该文件中包含了自动配置类

  10. Spring如何解决循环依赖问题?

 ```
 解决循环依赖的关键就是Spring的第三级缓存+Java反射。
 当采用构造器注入时,Spring无法解决基于构造器的循环依赖,这是因为new对象的时候就被堵住了,典型的先有鸡还是先有蛋问题,Spring会直接抛出异常。
 当采用基于setter方法且是原型bean时,Spring也无法解决循环依赖,这样会导致一直new对象直到OOM为止,因此Spring直接抛出异常。
 当单例Bean达到可用状态时(即实例化、填充、初始化都完成了),会将该单例Bean添加到一级缓存中去(同时移除二、三级缓存),下次再获取该单例Bean时,就会从一级缓存中获取,因此单例Bean始终是同一个对象。

 Spring三级缓存:
   三级缓存是互斥的,同一时间段bean实例只能存在于其中一个缓存中。
   一级缓存是暴露给用户的,二级、三级缓存是给spring内部使用的。
   一级缓存存放的是可以使用的bean实例,而二、三级缓存存放的是早期的bean实例(半成品,只new出了对象,但还未给属性赋值)
 二级缓存有什么用,可以去掉吗?
   三级缓存存放的是单例工厂,通过单例工厂的getObject方法可以产出bean实例。只要两个缓存确实可以做到解决循环依赖的问题,但是有一个前提这个bean没被AOP进行切面代理,如果这个bean被AOP进行了切面代理,那么只使用两个缓存是无法解决问题,因为如果没有AOP代理的话,那么getObject方法每次返回的都是同一个对象,而有AOP代理时,getObject方法每次返回的都是不同的代理对象,因此第一次调用getObject方法后,需要将此代理对象放入二级缓存中
 ```
  1. Spring Bean作用域?

    单例、原型、请求、会话、应用、自定义(比如每个Bean使用5次后就重新生成)

  2. Spring Bean生命周期?

    实例化、设置属性、执行Aware接口方法、前置处理、初始化、后置处理、使用、销毁

  3. 父子容器特点?

    父容器和子容器是相互隔离的,他们内部可以存在名称相同的bean;子容器可以访问父容器中的bean,而父容器不能访问子容器中的bean。防止service注入controller。

  4. BeanPostProcessor的作用?

    在对象实例化时运行。比如解密,AOP生成代理对象。

  5. BeanFactoryPostProcessor?

    在容器启动时运行。允许运行时修改Bean的配置元数据。比如用于数据源加密。

  6. AutoWired和Resource注解的区别?

    AutoWired通过类型查找,Resource先通过name查找,找不到再通过类型查找。

  7. Eureka的存储结构?

    注册表:
      ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
    缓存:
      ConcurrentMap<Key, ResponseCacheImpl.Value> readOnlyCacheMap
      LoadingCache<Key, ResponseCacheImpl.Value> readWriteCacheMap;
    
  8. Spring Boot如何实现自动装配?

    1、直接将自动配置类写在classpath:META-INF/spring.factories文件中。具体格式是org.springframework.boot.autoconfigure.EnableAutoConfiguration=?,?,?
    2、或是通过Import注解,导入一个Bean,导入一个Configuration类、导入实现ImportSelector的类。
    3、Spring Boot会默认扫描当前包下面的Bean。
    4、通过@CompnentScan
    
  9. service调用自己的方法时如何走AOP代理类?

    ((IUserService)AopContext.currentProxy()).save();
    
  10. AOP能代理getter/setter方法、能代理构造方法吗?

    getter/setter方法是可以代理的,但是构造器不能代理,代理类有自己单独的构造器。

  11. Spring Boot启动流程?

    1、首先初始化Spring的IOC容器(ApplicationContext)和环境、资源、监听器等;
    2、加载自动配置类,也就是classpath:META-INF/spring.factories;
    3、扫描启动类所在包下面的Bean。
    
  12. 如何手写Spring Boot的starter?

    通过spring.factories文件。

  13. Nacos和Eureka的区别?

    Nacos集成了配置中心;Eureka需要配合Spring Cloud Config实现配置中心

    Nacos下载后直接启动,而Eureka需要被引入到springboot项目中启动;

    Nacos需要采用MySql进行数据进行持久化;Nacos管理界面更美观,功能更多;

    Eureka需要配合MQ实现配置动态刷新,Nacos采用Netty保持TCP长连接实时推送;

  14. springboot启动时打印一句话,有哪些方式?

    main方法中打印

    监听ContextRefresh事件

    利用@Component且实现InitializingBean接口,或@Bean注解。SpringBoot的Bean默认是非懒加载的,可通过@Lazy注解使其懒加载

    实现CommandLineRunner接口

  15. Spring 框架中用到了哪些设计模式?请举例说明
    Spring框架中使用到了大量的设计模式,下面列举了比较有代表性的:
    代理模式:在AOP 和 remoting 中被用的比较多 。
    单例模式:在spring 配置文件中定义的 bean 默认为单例模式 。
    模板方法:用来解决代码重复的问题 。 比如 RestTemplate, JmsTemplate, JpaTemplate。
    前端控制器:Spring 提供了 DispatcherServlet 来对请求进行分发 。
    视图帮助 (View Helper ):Spring 提供了一系列的 JSP 标签,高效宏来辅助将分散的代码整合在视图里 。
    依赖注入:贯穿于 BeanFactory / ApplicationContext 接口的核心理念 。
    工厂模式:BeanFactory 用来创建对象的实例 。

8、Mybatis

  1. Mybatis二级缓存?

    一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。
    在sqlSession中执行相同的sql语句会从缓存中获取数据而不再从数据库查询,从而提高查询效率。当sqlSession结束后该sqlSession中的一级缓存也就不存在了。Mybatis默认开启一级缓存。 本地缓存不能被关闭,可以调用clearCache()来清空本地缓存,或者改变缓存的作用域。
    二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句时会从缓存获取,二级缓存是跨SqlSession的。其作用域是mapper的同一个namespace。Mybatis默认没有开启二级缓存需要在setting全局参数中配置开启二级缓存。 
    
  2. PageHelper分页原理?

    ThreadLocal存储分页参数+Mybatis通用拦截器。PageHelper会对紧跟其后的第一个查询语句实现分页,完成查询后会清除存储在ThreadLocal中的分页参数

  3. 为什么使用mybatis框架?

    代码量大大减少,开发效率高。
    MyBatis相当灵活,SQL写在XML里,从程序代码中彻底分离,降低耦合度,便于统一管理和优化,并可重用。
    运行效率高。

  4. Mybatis加载XML文件原理?

    1、首先,Mybatis在初始化 SqlSessionFactoryBean的时候,找到mapperLocations路径去解析里面所有的XML文件;
    2、Mybatis会把每个SQL标签封装成SqlSource对象。然后根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成,比如IfSqlNode、ForEachSqlNode;
    3、xml文件中的每一个SQL标签就对应一个MappedStatement对象,这里面有两个属性很重要:id(全限定类名+方法名组成的ID)、sqlSource(当前SQL标签对应的SqlSource对象);
    4、创建完MappedStatement对象,将它缓存到Configuration中。Configuration对象就是Mybatis中的大管家,基本所有的配置信息都维护在这里。把所有的XML都解析完成之后,Configuration就包含了所有的SQL信息;
    5、到目前为止,XML就解析完成了。当我们执行Mybatis方法的时候,就通过全限定类名+方法名找到MappedStatement对象,然后解析里面的SQL内容,执行即可。
    
  5. Mybatis Dao接口代理原理?

    1、通过@MapperScan注解扫描所有的Dao接口,且将它们的beanClass设置为 MapperFactoryBean。MapperFactoryBean 实现了 FactoryBean 接口;
    2、当我们通过 @Autowired 注入这个Dao接口的时候,返回的对象就是MapperFactoryBean 这个工厂Bean中的 getObject() 方法对象;
    3、getObject()方法通过JDK动态代理,返回了一个Dao接口的代理对象,这个代理对象的处理器是MapperProxy对象。所以,我们通过@Autowired注入Dao接口的时候,注入的就是这个代理对象,我们调用到Dao接口的方法时,则会调用到MapperProxy对象的invoke()方法(代理对象会拦截接口方法,根据类的全限定名+方法名,唯一定位到一个MapperStatement)。
    

  6. Dao接口里的方法,参数不同时,方法能重载吗?

    不能。因为是使用 全限名+方法名 的保存和寻找策略,来找到MapperStatement对象的。

  7. 模糊查询like语句该怎么写?

    第1种:在Java代码中添加sql通配符。

    <select id=”selectlike”>
     select * from foo where bar like #{value}
    </select>
    

    第2种:在sql语句中拼接通配符,会引起sql注入

    <select id=”selectlike”>
         select * from foo where bar like "%"${value}"%"
    </select>
    
  8. Mybatis $不能防止SQL注入,用于什么场景?

    比如数据权限过滤、分页之类的,必须将service层拼接生成的字段串替换到xml标签中去。

9、设计模式

  1. solid原则有哪些,策略模式解决什么问题?

    策略模式定义了算法家族,分别封装起来,让它们之间可以互相替换。

10、计算机网络

  1. TCP三次握手和四次挥手原理?

    为什么不是两次?因为TCP是全双工通信,除了要让A收到B的确认号,同时还要让B收到A的确认号,因此需要第三次握手。

    还有一个原因是,防止已失效的连接请求报文突然又传送到了B,产生错误。

    为什么是四次挥手?关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手

  2. HTTP 1.0、1.1、1.2的区别?

    1.1增加了长连接keep-alive,减少了创建和销毁TCP连接的开销。2.0增加了多路复用机制,同一个连接可以处理多个请求,提高了服务端并发处理能力

11、数据结构

  1. 讲解红黑树

    红黑树可以保证在最坏情况下时间复杂度为O(logn)。
    AVL是严格平衡树,因此在增加或者删除节点的时候,旋转的次数比红黑树要多。
    由于AVL高度平衡,Search效率更高。
    红黑树是对search、insert 、delete效率的折衷,总体来说,RB-Tree的统计性能高于AVL。
    红黑树性质:每个结点要么是红色要么是黑色;根结点和叶子结点NIL一定是黑色;红色结点的孩子都是黑色;每个结点到其所有后代叶子结点的路径上的黑色结点数相同。
    
  2. 冒泡排序和快排的比较,时间复杂度分别是多少?

    冒泡:最好O(n) 最坏O(n^2) 平均O(n^2) 辅助空间O(1) 稳定

    快排:最好O(nlogn) 最坏O(n^2) 平均O(nlogn) 辅助空间O(logn)~O(n) 不稳定

  3. 快速排序思想?

    1、从数组中选取一个元素作为基准值(一般是第一个),将待排序的数组分成左右两部分,左边的部分小于基准值,右边的部分大于基准值;
    2、使用左右两个指针向中间移动(可以约定右指针先移动),左边的指针移到比基准值大的下标时停下,右边的指针移动到比基准小的下标时停下,然后双方交换数组元素,继续向左、向右移动;
    3、当左右指针重合时,有两种情况:
      基准值大于重合处元素:将基准值与重合处元素交换,一轮结束,以重合处下标为分界线,分成左右两部分,递归排序;
      基准值小于等于重合处元素:将基准值与重合处上一个元素交换,一轮结束,以重合处上一个下标为分界线,分成左右两部分,递归排序;
    
  4. 如何判断链表有环?

    直接遍历即可,用一个HashSet存储,如果有相同的结点,则说明有环。且一定是尾部有环。

  5. 如何判断两个单链表是否相交?

    直接法:
      判断第一个链表的每个结点是否存在于第二个链表中。时间复杂度为O(length1*length2)
    方法2:
      对于两个没有环的链表相交于一节点,则在这个节点之后的所有结点都是两个链表所共有的。如果它们相交,则最后一个结点一定是共有的,则只需要判断最后一个结点是否相同即可。时间复杂度为O(length1+length2)。
    如何找出相交结点?
      求出两个链表的长度,然后用长的减去短的得到一个差值K,然后让长的链表先遍历K个结点,然后两个链表再开始比较
    

12、其它

  1. StringBuilder和SpringBuffer有啥区别?

    SpringBuffer每次toString都会使用toStringCache值来构造一个字符串,而StringBuilder则每次都需要复制一次字节数组,再构造一个字符串。

    StringBuilder是线程不安全的。StringBuffer直接在方法上加synchronized关键字保证线程安全。

  2. String能被继承吗?

    不能,因为String是被final修饰的。

  3. 为什么String是不可变的?

    因为String里面的char数组是用final修饰的。

  4. Linux命令

    操作文件目录的:ll ls pwd cd
    查看文件内容的:cat vi less more tail head sed
    grep ps scp nohup ssh yum systemctl top(查看CPU) free(查看内存) df(查看磁盘) zip tar ip ifconfig
    jps jstat java javac
    
  5. 序列化的原理?

    ObjectInputStream、ObjectOutputStream

    FastJSON、Jackson

    序列化ID决定是否能够成功反序列化,即用序列化校验是否一致。

  6. 分布式中的技术?

    负载均衡

    垂直扩展:提升硬件配置

    水平扩展:多态廉价的机器组成一个集群一起运作,将数据进行分片,分片规则有Hash分片和Range分片

    主从复制问题:如何保证一致性?一致性要求高的话,要采取同步复制,但会阻塞客户端请求

    P2P对等复制:通过选举(半数机制)选出leader,写数据往leader节点写,读数据leader和follower都可以

    一致性协议:一致性Hash、redis的哈希槽

    分布式事务

    分布式缓存

    分布式锁

    CAP原理

    酸碱中和理论ACID+BA(基本可以)+S(软状态)+E(最终一致性)

  7. 正排索引与倒排索引?

    正规化(时态的转换,单复数的转换,同义词的转换,大小写的转换)

  8. 什么是反射?

    可以在运行期间获取对象的Class对象信息。可通过Class对象创建一个实例,也可以获取属性信息、方法信息、构造方法信息等。

  9. 基于RBAC实现数据权限?

    https://wwwblogs/jianzh5/p/14486547.html

  10. 常见的的网络安全漏洞,有没有了解安全漏洞问题?

    SQL注入漏洞,比如select 1 from user where username=admin and password=123 or 1=1

    xss(跨站脚本攻击,攻击者嵌入恶意脚本代码到正常用户会访问到的页面中,当正常用户访问该页面时,导致嵌入的恶意脚本代码的执行 )

    CSRF(跨站脚本伪造,攻击者盗用了你的身份,以你的名义发送恶意请求 )

  11. 如何防止SQL注入?
    预编译prepareStatement, mybatis用 #{} 而非${}

  12. hashCode和equals?重写规则

    hashcode相等,equals不一定相等

    hashcode不等,equals一定不等

    equals相等,hashcode一定相等

    equals不等,hashcode不一定不等

  13. 怎么创建一个类的实例?

    new、反射(Class对象的newInstance方法或Constructor)、克隆、反序列化

  14. 一个类的加载顺序,父类,子类,静态代码块,实例代码块

    父类静态代码块,子类静态代码块,父类实例代码块,父类构造方法,子类实例代码块,子类构造方法

  15. 如何用poi兼容excel版本?

    里面有不同的类分别兼容不同版本。

  16. kill和kill -9的区别?

    一般不加参数kill是使用15来杀,这相当于正常停止进程,停止进程的时候会释放进程所占用的资源;

    kill - 9表示强制杀死该进程

    如果是分布式锁还有可能导致死锁。

  17. linux写时复制技术
    第一代Unix系统实现了一种傻瓜式的进程创建:当执行fork系统调用时,内核复制父进程的整个用户空间并把复制得到的那一份分配给子进程。
    这种行为时非常耗时的,因为它需要完成以下几项任务:
    1、为子进程的页表分配页面
    2、为子进程的页分配页面
    3、初始化子进程的页表
    4、把父进程的页复制到子进程对应的页中
    写时复制(copy-on-write)是一种可以推迟甚至避免复制数据的技术。内核此时并不是复制整个进程空间,而是让父进程和子进程共享同一个副本。
    只有在需要写入的时候,数据才会被复制,从而使父进程、子进程拥有各自的副本。也就是说,资源的复制只有在需要写入的时候才进行,
    在此之前以只读方式共享。这种技术使得对地址空间中的页的复制被推迟到实际发生写入的时候。有时共享页根本不会被写入,
    fork()实际开销就是复制父进程的页表以及给子进程创建唯一的PCB(进程控制块)。这种优化可以避免复制大量根本就不会使用的数据。

  18. 什么是伪共享与缓存行填充?

    一、伪共享的定义:

    伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

    二、CPU缓存机制

    CPU 缓存的百度百科定义为:

    CPU 缓存(Cache Memory)是位于 CPU 与内存之间的临时存储器,它的容量比内存小的多但是交换速度却比内存要快得多。
    高速缓存的出现主要是为了解决 CPU 运算速度与内存读写速度不匹配的矛盾,因为 CPU 运算速度要比内存读写速度快很多,这样会使 CPU 花费很长时间等待数据到来或把数据写入内存。
    在缓存中的数据是内存中的一小部分,但这一小部分是短时间内 CPU 即将访问的,当 CPU 调用大量数据时,就可避开内存直接从缓存中调用,从而加快读取速度。
    CPU 和主内存之间有好几层缓存,因为即使直接访问主内存也是非常慢的。如果你正在多次对一块数据做相同的运算,那么在执行运算的时候把它加载到离 CPU 很近的地方就有意义了。

    按照数据读取顺序和与 CPU 结合的紧密程度,CPU 缓存可以分为一级缓存,二级缓存,部分高端 CPU 还具有三级缓存。每一级缓存中所储存的全部数据都是下一级缓存的一部分,越靠近 CPU 的缓存越快也越小。所以 L1 缓存很小但很快(译注:L1 表示一级缓存),并且紧靠着在使用它的 CPU 内核。L2 大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。L3 在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。最后,你拥有一块主存,由全部插槽上的所有 CPU 核共享。拥有三级缓存的的 CPU,到三级缓存时能够达到 95% 的命中率,只有不到 5% 的数据需要从内存中查询。

    多核机器的存储结构如下图所示:

 当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,然后是 L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要确保数据在 L1 缓存中。

 Martin Thompson 给出了一些缓存未命中的消耗数据,如下所示:

 三、缓存行

 缓存系统中是以缓存行(cache line)为单位存储的。缓存行通常是 64 字节(译注:本文基于 64 字节,其他长度的如 32 字节等不适本文讨论的重点),并且它有效地引用主内存中的一块地址。例如一个 的 long 类型是 8 字节,因此在一个缓存行中可以存 8 个 long 类型的变量。所以,如果你访问一个 long 数组,当数组中的一个值被加载到缓存中,它会额外加载另外 7 个,以致你能非常快地遍历这个数组。事实上,你可以非常快速的遍历在连续的内存块中分配的任意数据结构。而如果你在数据结构中的项在内存中不是彼此相邻的(如链表),你将得不到免费缓存加载所带来的优势,并且在这些数据结构中的每一个项都可能会出现缓存未命中。

 如果存在这样的场景,有多个线程操作不同的成员变量,但是相同的缓存行,这个时候会发生什么?。没错,伪共享(False Sharing)问题就发生了!有张 Disruptor 项目的经典示例图,如下:

 上图中,一个运行在处理器 core1上的线程想要更新变量 X 的值,同时另外一个运行在处理器 core2 上的线程想要更新变量 Y 的值。但是,这两个频繁改动的变量都处于同一条缓存行。两个线程就会轮番发送 RFO 消息,占得此缓存行的拥有权。当 core1 取得了拥有权开始更新 X,则 core2 对应的缓存行需要设为 I 状态。当 core2 取得了拥有权开始更新 Y,则 core1 对应的缓存行需要设为 I 状态(失效态)。轮番夺取拥有权不但带来大量的 RFO 消息,而且如果某个线程需要读此行数据时,L1 和 L2 缓存上都是失效数据,只有 L3 缓存上是同步好的数据。从前一篇我们知道,读 L3 的数据非常影响性能。更坏的情况是跨槽读取,L3 都要 miss,只能从内存上加载。

 表面上 X 和 Y 都是被独立线程操作的,而且两操作之间也没有任何关系。只不过它们共享了一个缓存行,但所有竞争冲突都是来源于共享。

 因此,当两个以上CPU都要访问同一个缓存行大小的内存区域时,就会引起冲突,这种情况就叫“共享”。但是,这种情况里面又包含了“其实不是共享”的“伪共享”情况。比如,两个处理器各要访问一个word,这两个word却存在于同一个cache line大小的区域里,这时,从应用逻辑层面说,这两个处理器并没有共享内存,因为他们访问的是不同的内容(不同的word)。但是因为cache line的存在和限制,这两个CPU要访问这两个不同的word时,却一定要访问同一个cache line块,产生了事实上的“共享”。显然,由于cache line大小限制带来的这种“伪共享”是我们不想要的,会浪费系统资源。

 四、如何避免伪共享? 

 1)让不同线程操作的对象处于不同的缓存行。

 可以进行缓存行填充(Padding) 。例如,如果一条缓存行有 64 字节,而 Java 程序的对象头固定占 8 字节(32位系统)或 12 字节( 64 位系统默认开启压缩, 不开压缩为 16 字节),所以我们只需要填 6 个无用的长整型补上6*8=48字节,让不同的 VolatileLong 对象处于不同的缓存行,就避免了伪共享( 64 位系统超过缓存行的 64 字节也无所谓,只要保证不同线程不操作同一缓存行就可以)。

 2)使用编译指示,强制使每一个变量对齐。

 强制使对象按照缓存行的边界对齐。例如可以使数据按64位对齐,那么一个缓存行只有一个可操作对象,这样发生伪共享之后,也只是对应缓存行的数据变化,并不影响其他的对象。

 因此,在线性查询数组和链表中的某一个元素时,线性查询数组效率会更高一点。
  1. 什么是 Busy spin?我们为什么要使用它?

    Busy spin 是一种在不释放 CPU 的基础上等待事件的技术。它经常用于避免丢失 CPU 缓存中的数据(如果线程先暂停,之后在其他CPU上运行就会丢失)。所以,如果你的工作要求低延迟,并且你的线程目前没有任何顺序,这样你就可以通过循环检测队列中的新消息来代替调用 sleep() 或 wait() 方法。它唯一的好处就是你只需等待很短的时间,如几微秒或几纳秒。LMAX 分布式框架是一个高性能线程间通信的库,该库有一个 BusySpinWaitStrategy 类就是基于这个概念实现的,使用 busy spin 循环 EventProcessors 等待屏障

13、情景题

  1. 同时有100个人提交表单,表单有普通参数和文件上传,会有什么问题?怎么解决?

    文件上传是一个耗CPU的操作,而且是同时有多个文件上传,带宽也会增加。
    如果用队列将文件处理改成同步的话,带宽问题还是解决不了。
    并不是所有的文件都真实的上传到了服务器,比较通用的方法是在上传之前校验文件的MD5,这样如果侦测到相同MD5的文件,就做一个软指针,指向服务器端已存在的文件即可,实现秒传的效果。
    至于用户文档之类比较定制化的文件,一般体积都不会很大,这种服务器会限制每个用户的上传带宽,比如100KB/S
    或者也可以做动态负载均衡,入口带宽1000M,十个人使用每人100M,一百个人使用每人10M这种。
    或者限制同一时间内的文件上传线程数 。
    增加带宽、增加服务器、限制用户上传文件的大小。
    普通参数和文件分别请求到不同服务器。
    
  2. 如何分表,按什么字段,订单号、UserID,还是订单时间?分表后如何查前10条,如何查11到20条?某个用户订单量特别多数据倾斜如何解决?

    如果想快速查询某个用户的所有菜单,按用户分表比较好。

    分表后查前10条的解决办法:查询每个结点的前10条,汇总后取前10条

    分表后查11-20条的解决办法:查询每个结点的前20条,汇总后取11-20条

  3. 深圳发往纽约,深圳到杭州、杭州到上海、上海到纽约分别用三个物流商(便宜),当用户查询物流轨迹信息时,需要调用三个运营商的接口,为了实时性更高,采用多线程异步调用,用CountDownLatch计数工具来等待接口调用完成。假设某个物流商接口报错,则需要做容错来给一个默认的轨迹信息,CountDownLatch也要做超时不能一直等下去。

  4. 一亿条数据,如何快速取前1000条?

    堆排序

    分表,然后局部排序取1000条,最后汇总取1000条

  5. 500M内存,10个10G的大日志文件(有序)如何整合排序?

    思路:归并排序、外部排序、多线程

  6. 第一天工作量是1,后面每一天是前面所有天的工作量总和,一共工作30天,问第25天的工作量占所有工作量的几分之几?

    2的(30-25+1)次方分之一

  7. 限流算法有哪些?令牌桶、信号量的关系?比如控制1分钟内最多100个请求通行,但是第一秒就100个了,如何解决?

    把时间粒度设小一点,设成1秒最多10个请求

  8. 第一天工作量是1,后面每一天是前面所有天的工作量总和,一共30天,问第25天的工作量占所有工作量的几分之几

    第n天的工作量为:2^(n-2) (n>1),第30天的工作量为228,总工作量为229,第25天工作量为2^23,则最终答案为1/64

14、IO

NIO示意图,如下图所示。

BIO程序的问题:

  • 服务端accept()方法接口请求,是阻塞的,用while循环去接收请求,接收到请求后,处理请求
  • read()方法获取请求的数据,但read()如果读不到数据,就会一直阻塞,这样就不能accept到下一个客户端,这在高并发场景下是有问题的
  • 也可以接收到一个请求之后,new一个线程去处理,但这样会出现C10K(单机1万连接)、C10M(单机1千万连接)问题
    可以用线程池代替,假设线程池最大线程数是500,那假设这500个线程一直读不到客户端数据,一直阻塞在read方法上,那就白白浪费了资源

如何用NIO优化BIO程序?

  • 由于ServerSocket的accept方法和Socket的read方法会产生阻塞,因此需要将ServerSocket和Socket注册到多路复用器Selector上
  • Selector内部维护一个channel列表,另外还需要维护一个就绪事件列表,调用Selector的select方法会产生阻塞,直至有事件产生,然后将该channel移至就绪事件列表

Linux内核函数:

  • epoll_create:创建一个epoll
  • epoll_ctl:将channel与事件绑定起来
  • epoll_wait:阻塞,没有事件产生就会阻塞。如何感知事件?网口感知到客户端请求后,由操作系统的中断程序提醒事件就绪

Netty线程模型(Reactor响应式编程设计模式):

  • 假设只有一个selector,当从select方法中被操作系统中断程序唤醒后,假设有5万个事件需要处理,那再怎么快也需要几秒甚至几十秒,这样会造成新的连接一直在等待,使得新的用户体验非常差
  • 因此,主从Reactor就出来了,主Reactor负责处理连接,处理好之后交给从Reactor,从Reactor负责处理读写事件
  • 由于连接是非常快的,而读写是非常慢的,因此一般会使用一主多从的Reactor模式

Redis6.0核心的数据处理逻辑仍然是单线程的,只不过数据的首发、RESP协议的解析采用了多线程。

Redis为什么不用Netty?因为Netty是多线程的。

使用Netty而不使用Java原生NIO的原因?

开发出高质量的NIO 程序并不是一件简单的事情,除去NIO固有的复杂性和Bug不谈,作为一个NIO服务端,需要能够处理网络的闪断、客户端的重复接入、客户端的安全认证、消息的编解码、半包读写等情况,如果没有足够的NIO编程经验积累,一个NIO框架的稳定往往需要半年甚至更长的时间。更为糟糕的是,一旦在生产环境中发生问题,往往会导致跨节点的服务调用中断,严重的可能会导致整个集群环境都不可用,需要重启服务器,这种非正常停机会带来巨大的损失。

从可维护性角度看,由于NIO采用了异步非阻塞编程模型,而且是一个I/O线程处理多条链路,它的调试和跟踪非常麻烦,特别是生产环境中的问题,我们无法进行有效的调试和跟踪,往往只能靠一些日志来帮助分析,定位难度很大。

对于java原生的IO我们之所以不选择使用是因为:
a)NIO的类库和API繁杂使用麻烦,你需要熟练掌握Selectol,ServerSocketChannel,SocketChannel,ByteBuffer等。
b)需妥具备其他的额外技能做制垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多钱程和网络编程非常如悉,才能编写出高质量的NIO程序。
c)可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络间断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NI0 编程的特点是功能开发相对容易,但是可靠性能力补齐的工作量和难度都非常大。
d)JDK NIO的BUG,比如epoll bug,这个BUG会在linux上导致cpu 100%,使得nio server/client不可用,这个BUG直到jdk 6u4才解决,但是直到JDK1.7中仍然有这个问题,该问题并未被完全解决,只是发生的频率降低了而已。

基于上述原因大多数场景下都不建议直接使原生NIO,除非你精通NIO编程或者是有特殊的需要,否则作为服务器编程的NIO可能会带来巨大的生产隐患。

关于Netty:
Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。

与Netty同样功能的NIO框架还有Mina,Netty的主导作者与Mina的主导作者是同一人,在设计理念上与Mina基本上是一致的。Mina出身于开源界的大牛
Apache,Netty出身于商业开源大亨Jboss。这几年Netty社区相对比较活跃,所以我们就先选择Netty作为入手网络编程的首选,有时间再学习一下Mina

TCP粘包与拆包问题解决方案:
1、消息定长,如每个报文大小固定为200字节,如果不够则补空格
2、包尾增加换行符,如FTP协议
3、消息分为消息头和消息体,消息头中包含消息总长度
4、更复杂的应用层协议
Netty通过LineBasedFrameDecoder解决TCP粘包问题,原理是通过\n或\r\n分隔,Netty服务端和客户端应使用相同的解码器
Netty提供了多种TCP粘包/拆包的编码器,用来满足用户的不同需求

Netty支持多种通信协议:Socket、Http、Websocket、UDP、文件传输
Reactor三种模型:Reactor单线程模型、Reactor多线程模型、主从Reactor多线程模型。Netty支持Reactor三种模型

15、分布式与高并发

  1. 秒杀场景,一是可以借助数据库的update语句加行锁,但这样会导致服务和数据库效率低下,如何解决?

    可以考虑使用redis做库存预扣,然后再用消息队列补偿。

  2. 分布式系统生成唯一ID方案汇总?

    1、数据库自增长序列或字段
      优点:简单、有序
      缺点:
        不同数据库或版本其语法或实现不同,在迁移数据库时要考虑这一点。
        强依赖DB,容易出现单点故障(单机版或只有一个主节点时)。
        DB水平扩展或多个主节点较难。解决方案:步长一样,起始值不同,但问题又来了,扩容怎么办?
        不适用于高并发场景。
    2、UUID(8-4-4-4-12,时间戳+时钟序列+MAC地址,GUID是UUID的实现)。
      优点:生成简单;本地生成,性能好,无网络消耗;全球唯一。
      缺点:无序(Mysql官方要求主键越短越好且有序);不可读;占用空间大;信息不安全(造成MAC地址泄露)
    3、Redis的INCR和INCRBY命令
    4、雪花算法
    
  3. 高并发场景下如何生成订单号?

    业务系统对订单号的要求:
      1、全局唯一性,这是最基本的
      2、趋势递增
      3、单调递增(与4是互斥的)
      4、信息安全:即无规则、不连续。如果是连续的,则竞争对手可以在昨天的12点和今天的12点下单,然后对两次的订单号相减,就可以得出一天的单量了。
      5、ID生成系统的可用性极高,可用性5个9(即一年的99.999%都是可用的),支持高QPS,延迟要低
    类snowflake方案(划分命名空间):
      1bit(不用)+41bit(时间戳)+10bit(机器ID)+12bit(自增序列号)
      优点:
        时间戳在高位,自增序列在低位,趋势递增
        不依赖第三方中间件,以服务方式运行
        可以根据业务特点分配bit位,非常灵活
      缺点:强依赖机器时钟
      举例:MongoDB的objectID
    Leaf-segment数据库方案(是对数据库方案的优化):趋势递增的ID,同时ID号是可计算的,不适用于订单ID生成场景
    Leaf-snowflake方案
      在集群节点数少时可手动配置workId,否则,可以在服务启动时通过zookeeper顺序节点拿到workId,且对zookeeper是弱依赖(本地缓存workId文件)
      这种方案依赖时间,如果机器时钟发生回拨,则有可能生成重复ID,需要解决时钟回退的问题:
        如果之前有写过workId节点,则每隔3s用自身系统时间与zk上的节点记录时间做比较,如果小于则发生了回拨,此时需要报警
        如果之前没有写过,则创建zk顺序节点并写入自身系统时间,并与其它节点的系统时间做一次比对(拿到zk上其它服务节点的IP:port,利用RPC获得各服务的系统时间,计算平均值,拿自己的系统时间与平均值做比对),若偏差大于某个阈值,则服务启动失败
    原文:https://tech.meituan/2017/04/21/mt-leaf.html
    
  4. 如何保证接口的幂等性?

    https://wwwblogs/aspirant/p/11628654.html

2、笔试题

1、定义一个List,插入100个随机数字(0~100之间的整数)。对该List进行去重,并按照从小到大排序。(去重可用set相关方法,排序不允许使用list自带方法或工具类,用单个元素操作实现)

public void question1(){
        ArrayList<Integer> list = new ArrayList();
        HashSet<Integer> set = new HashSet();

        Random r = new Random();
        for (int i = 0; i < 100; i++) {
            int j = r.nextInt(100);
            if (set.add(j)){
                list.add(j);
            }
        }
        sort(list);
        list.stream().forEach(System.out::println);
    }
    public void sort(List<Integer> list){
        for (int i = 0; i < list.size(); i++) {
            for (int j = 0; j < list.size()-i-1; j++) {
                if(list.get(j) > list.get(j+1)){
                    int temp = list.get(j);
                    list.set(j,list.get(j+1));
                    list.set(j+1,temp);
                }
            }
        }
    }

2、递归实现1+2+3+4+….+100

public void question2(){
        System.out.println(sum(100));
    }

    public int sum(int i){
        if(i == 1){
            return i;
        }
        return i+sum(i-1);
    }

3、如下表,请用一条sql写出,选出表中NAME相同的记录大于等于3条的对应ID集合

IDNAME
1A
2B
3B
4B
5C
6D
7D
8D
select id from tab where name in (select name from tab group by name having count(name) >= 3)

4、设计一段程序,让一个数组的奇数都在偶数左边。

借鉴快排的思路,用左右两个指针分别指向数组的头和尾,相向移动,左指针遇到偶数时定下来,右指针遇到奇数时停下来,交换后再移动,直到重合。

5、for循环的执行顺序?

for(A;B;C){
  D
}

执行顺序是A->B->D->C->B->D->C->…

6、Integer类缓存机制?

Integer f1=100,f2=100,f3=150,f4=150;
System.out.println(f1==f2); // true。在128以内,从缓存获取,故是同一个对象
System.out.println(f3==f4); // false

更多推荐

精心整理200道最新Java中高级工程师高频面试题