Java八股文大总结

在这里插入图片描述
  本篇用于记录总结Java八股面试常问问题,并对相应知识进行扩展学习,每个问题下会持续更新我认为写的不错的文章作为扩展阅读。预祝大家面试中能激情吟唱。

  • 本篇内容持续更新,关注收藏不迷路。
  • 若需要 w o r d word word 版本自定义扩充,进行打印记忆复习,请评论
  • 参考指路指南: JavaGuide | 小林Coding | 程序员大彬 | 二哥的Java进阶之路 | 黑马
    🍕  🎉  🥓  🎊  🚗  🐷  😱  ❤  😀  ❣  🥙  🎇  🕵️‍♀️

一、Java基础篇

【1】面向对象编程有哪些特性?

面向对象有四大特性:封装继承多态

  1. 封装就是将类的信息隐藏在类的内部,不允许外部程序直接访问,而是通过该类的getXxx()setXxx()方法实现对隐藏信息的操作和访问。良好的封装能够减少耦合
  2. 继承是从已有的类中派生出新的类,新的类继承父类的属性和行为,并能扩展新的能力,大大增加程序的重用性易维护性。在Java中是单继承的,也就是说一个子类只能有一个父类。
  3. 多态同一个行为具有多个不同表现形式的能力。在不修改程序代码的情况下改变程序运行时绑定的代码。实现多态的三要素:继承重写父类引用指向子类对象
    • 静态多态性:通过重载实现,相同的方法有不同的参数列表,可以根据参 数的不同,做出不同的处理。
    • 动态多态性:在子类中重写父类的方法。运行期间判断所引用对象的实际 类型,根据其实际类型调用相同的方法。

扩展阅读:面向对象编程


【2】什么是值传递和引用传递?

  1. 值传递是对基本类型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量。
  2. 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身,两者指向同一片内存空间。所以对引用对象进行操作会同时改变原对象
  3. Java中不存在引用传递,只有值传递。即不存在变量a指向变量b,变量b指向对象的这种情况。

扩展阅读:Java到底是值传递还是引用传递?

【3】了解Java的包装类型吗?为什么需要包装类?
  Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们无法将int、double等类型放进去,因为集合的容器要求元素是Object类型,此外,泛型也必须使用包装类。
  为了让基本类型也具有对象的特性,就出现了包装类型。相当于将基本类型包装起来,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

扩展阅读:有了基本数据类型,为什么还需要包装类型


【4】什么是自动装箱和拆箱?
装箱:基础类型 — > > > 包装类型;
拆箱:包装类型 — > > > 基本类型;
如当具有以下操作时,编译器会自动帮我们进行装箱或拆箱:

  1. 赋值操作(装箱:Integer x = 1; 、 拆箱:int y = x;);
  2. 进行加减乘除混合运算(拆箱);
  3. 进行><== 比较运算(拆箱);
  4. 调用equals进行比较(装箱)
  5. ArrayList、HashMap等集合类添加基础类型数据时(装箱)

扩展阅读:深入剖析Java中的装箱和拆箱


【5】两个值相等的Integer用==比较不相等的原因?
  若两个数不在-128~127之间,则会显示false,因为常量池会缓存-128~127之间的数,若创建的数在-128~127之间,那么会从常量池中取出该地址返回给对象,因此下面代码的ab 指向的是相同的地址。若创建的数不在-128~127之间,如下面的代码cd200,其不在常量池中,因此会new Integer()并返回,因此cd其实是new了两个不同对象,因此地址不同,==比较结果也会不同。

Integer a = 100;
Integer b = 100;
System.out.println(a == b);  // true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);  // false

扩展阅读:两个Integer 用== 比较不相等的原因


【6】String为什么不可变?
  不可变对象的定义:如果一个对象在它创建完成之后,不能改变它的状态,那么这个对象就是不可变的。不能改变状态的意思是,不能改变对象内的成员变量,包括基本数据类型的值不能改变引用类型的变量不能指向其他对象引用类型指向的对象的状态也不能改变

  将String设计成不可变的原因如下:

  1. 线程安全:同一个字符串实例可以被多个线程共享时,因为字符串不可变,本身就是线程安全的。
  2. 支持hash映射和缓存:因为Stringhash值经常会使用到,比如作为Map的键,不可变的特性使得hash值也不会变,不需要重新计算。
  3. 出于安全考虑:网络地址URL、文件路径Path、密码等通常以String类型保存,若String可变,将会引起安全隐患。
  4. 字符串常量池优化String对象创建之后,会缓存到字符串常量池中,下次需要创建同样的对象时,可以直接返回缓存的引用

String内部的substringreplacereplaceAll等方法看似会改变String对象,作何解释?其实这些方法会在堆内存中创建一个新对象,然后将其String类的value数组引用指向不同的对象。

扩展阅读String底层为什么不可变?String为什么不可变?String底层不可变原理,不明白?


【7】String、StringBuffer、StringBuilder的区别?

  1. 可变性
    • String不可变
    • StringBuffer和StringBuilder可变
  2. 是否线程安全
    • String线程安全
    • StringBuilder线程不安全
    • StringBuffer是线程安全的,其内部使用Synchronized进行同步

扩展阅读:String、StringBuffer和StringBuilder类的区别

【8】String类的常用方法有哪些?

  1. indexOf(): 返回指定字符的索引;
  2. charAt(): 返回指定索引处的字符;
  3. replace(): 字符串替换;
  4. trim(): 去除字符串两端空白;
  5. split(): 分割字符串,返回一个分割后的字符串数组;
  6. getBytes(): 返回字符串的byte类型数组;
  7. length(): 返回字符串长度;
  8. toLowerCase(): 将字符串转成小写字母;
  9. toUpperCase(): 将字符串转成大写字符;
  10. substring(): 截取字符串;
  11. equals(): 字符串比较。

【9】new String(“lwz”)会创建几个对象?
  使用这种方式会创建两个字符串对象(前提是字符串常量池中没有"lwz"这个字符串对象);

  1. "lwz"属于字符串字面量,因此编译时期会在字符串常量池中创建一个字符串对象,指向"lwz"字符串字面量;
  2. 使用new的方式会在堆中创建一个字符串对象。

扩展阅读:new String(“ab“)到底创建了几个对象?new String(“a“) + new String(“b“)呢?


【10】什么是字符串常量池?
  字符串常量池保存着所有字符串字面量,这些字面量在编译时期就确定。字符串常量池位于堆内存中,专门用来存储字符串常量。在创建字符串时,JVM首先会检查字符串常量池,如果该字符串已经存在池中,则返回其引用,如果不存在,则创建此字符串且放入池中,并返回其引用。

扩展阅读:正确理解和使用JAVA中的字符串常量池


【11】Object常用方法有哪些?

  1. toString(): 默认输出对象地址
  2. equals(): 默认比较两个引用变量是否指向同一个对象(内存地址)。
  3. hashCode(): 将与对象相关的信息映射成一个哈希值,默认实现hashCode值是根据内存地址换算出来的。
  4. clone(): 实现了对象中各个属性的复制,但它的可见范围是protected的。
  5. getClass(): 返回此 Object 的运行时类,常用于 Java 反射机制。
  6. wait(): 当前线程调用对象的 wait() 方法之后,当前线程会释放对象锁,进入等待状态。等待其他线程调用此对象的 notify()/notifyAll()唤醒或者等待超时时间 wait(long timeout) 自动唤醒。线程需要获取 obj 对象锁之后才能调用 obj.wait()
  7. notify(): obj.notify()随机唤醒在此对象上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象上等待的所有线程。

【12】讲讲深拷贝和浅拷贝
浅拷贝: 拷贝对象和原始对象的引用类型引用同一个对象
深拷贝: 拷贝对象和原始对象的引用类型引用不同对象

扩展阅读:Java深拷贝和浅拷贝


【13】两个对象的hashCode()相同,则equals()是否也一定为true?

equals()hashCode()的关系:

  1. 如果两个对象调用equals()比较返回true,那么它们的hashCode()值一定要相同;
  2. 如果两个对象的hashCode()相同,它们并不一定相同(有可能是发生了哈希碰撞);

hashcode()方法主要是用来提升对象比较的效率,先进行hashcode()的比较,如果不相同,那就不必再进行equals()的比较,这样就大大减少了equals()比较的次数,当比较对象的数量很大的时候能提升效率。

扩展阅读:两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?


【14】为什么重写equals时一定要重写hashCode?

 之所以重写equals()要重写hashCode(),是为了保证equals()方法返回true的情况下hashcode()值也要一致,如果重写了equals()没有重写hashcode(),就会出现两个对象相等但hashcode()不相等的情况。这样当用其中的一个对象作为键保存到hashMaphashTablehashSet时,再以另一个对象作为键值去查找他们的时候,则会查不到。

扩展阅读:终于搞懂为什么重写equals就需要重写hashCode了


【15】Java创建对象有几种方式?

 Java创建对象有以下几种方式:

  1. new语句创建对象;
  2. 运用反射手段,调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法;
  3. 调用对象的clone()方法;
  4. 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。

扩展阅读:Java 创建对象的几种方式


【16】类实例化的顺序

 父类静态变量和静态代码块— > > >子类静态变量和静态代码块— > > >父类成员变量和普通代码块— > > >父类构造方法— > > >子类成员变量和普通代码块— > > >子类构造方法。
 其中,静态代码块只会被执行一次,不会因为多次 new 而再次执行。

扩展阅读:由浅入深详解Java 类的实例化顺序


【17】equals和==有什么区别?

  1. 对于基本数据类型,==比较的是他们的。基本数据类型没有equal方法;
  2. 对于引用数据类型,==比较的是它们的存放地址(是否是同一个对象)。equals()默认比较地址值,重写的话按照重写逻辑去比较。

【18】常见重要关键词

  1. static:可以用来修饰类的成员方法、类的成员变量
    (1) static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象共享,在内存中只有一个副本,只在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
    (2) static方法称为静态方法。静态方法不依赖于任何对象就能进行访问,通过类名即可调用静态方法。
    (3) 静态代码块只会在类加载的时候执行一次
    (4) 静态内部类,使用非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。而静态内部类不需要。
  2. final
    (1) 基本数据类型用final修饰,则不可变,是常量,而且必须被初始化;对象引用用final修饰,则引用只能指向该对象,不能指向别的对象,但是对象本身可以修改
    (2) final修饰的方法不能被子类重写
    (3) final修饰的类不能被继承
  3. this
    (1) this.属性名称:指访问类中的成员变量,可以用来区分成员变量和局部变量。
    (2) this.方法名:用来访问本类的方法。
  4. super:用于在子类中访问父类的成员变量和方法

参考:final,static,this,super 关键字总结


【19】方法重载和重写的区别?

  1. 同个类中的多个方法可以有相同的方法名称,但是有不同的参数列表,这就称为方法重载。参数列表又叫参数签名,包括参数的类型、参数的个数、参数的顺序,只要有一个不同就叫做参数列表不同。重载是面向对象的一个基本特性。
  2. 方法重写描述的是父类和子类之间的。当父类的功能无法满足子类的需求,可以在子类对方法进行重写。方法重写时,方法名、形参列表、方法返回值必须一致

扩展阅读:重载(Overload)和重写(Override)的区别


【20】throw和throws的区别?

  1. throw:用于抛出一个具体的异常对象
  2. throws:用在方法签名中,用于声明该方法可能抛出的异常。子类方法抛出的异常范围更小,或者根本不抛出异常。

扩展阅读:throw和throws的区别


【21】接口与抽象类的区别

  1. 语法层面上的区别
    • 抽象类可以有方法实现,而接口的方法中只能是抽象方法
    • 抽象类中的成员变量可以是各种类型的,接口中的成员变量只能是public static final类型;
    • 接口中不能含有静态代码块以及静态方法,而抽象类可以有
    • 一个类只能继承一个抽象类,而一个类却可以实现多个接口
  2. 设计层面上的区别
    • 抽象层次不同。抽象类是对整个类整体进行抽象,包括属性行为,但是接口只是对类行为进行抽象。继承抽象类是一种“是不是”的关系,而接口实现则是“有没有”的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是具备不具备的关系。
    • 继承抽象类的是具有相似特点的类,而实现的接口却可以不同的类。

扩展阅读:接口的概念与使用 接口和抽象类的区别


【22】BIO/NIO/AIO之间的区别?

  1. 同步阻塞IO(BIO):用户进程发起一个 IO 操作以后,必须等待 IO 操作的真正完成后,才能继续运行;
  2. 同步非阻塞IO(NIO):客户端与服务器通过Channel链接,采用多路复用器轮询注册的Channel。提高吞吐量和可靠性。用户进程发起一个 IO 操作以后,可以做其他事情,但用户进程需要轮询IO操作是否完成,这样造成了不必要的CPU资源浪费
  3. 异步非阻塞IO(AIO)非阻塞异步通信模式,NIO的升级版,采用异步通道实现异步通信,其readwrite方法均是异步方法。用户进程发起一个 IO 操作,然后立即返回,等IO操作真正的完成以后,应用程序会得到 IO 操作完成的通知。

扩展阅读:如何理解BIO、NIO、AIO的区别?


【23】守护线程是什么?

  1. 守护线程是运行在后台的一种特殊线程。
  2. 独立于控制终端并且周期性的执行某种任务等待处理某些发生的事件
  3. 在Java中垃圾回收线程就是特殊的守护线程。

扩展阅读:守护线程是什么?守护线程和非守护线程的区别是?守护线程的作用是?


【24】同步和异步的区别?

  1. 同步是指当发出一个调用后,在没有得到结果之前,该调用就不返回,一直等待;
  2. 异步指在调用发出后,就可以干其他事了,被调用者返回结果之后会通知调用者,或通过回调函数处理这个调用。
  3. 同步和异步最大的区别就在于:同步需要等待,异步不需要等待

【25】阻塞和非阻塞的区别?

  1. 阻塞和非阻塞关注的是线程的状态
  2. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会恢复运行。
  3. 非阻塞调用指在不能立即得到结果之前,该调用不会阻塞当前线程

扩展阅读:理解:什么是同步和异步?什么是阻塞和非阻塞?


【26】什么是序列化和反序列化?

  1. 序列化:把对象转换为字节序列的过程称为对象的序列化;
  2. 反序列化:把字节序列恢复为对象的过程称为对象的反序列化;

扩展阅读:Java序列化与反序列化三连问:是什么?为什么要?如何做?


【27】transient关键字的作用?

  1. Java语言的关键字,变量修饰符,如果用transient声明一个实例变量,当对象存储时,它的值不需要维持。
  2. 也就是说被transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后,transient变量的值被设为初始值,如int型的是0,对象型的是null

【28】什么是反射?

  1. 在Java程序运行时,动态获取对象的信息以及动态调用对象的方法功能称为Java语言的反射机制。
  2. 在运行状态中,对于任意一个类,能够知道这个类的所有属性和方法。对于任意一个对象,能够调用它的任意一个方法和属性
  3. 优点: 增加程序的灵活性、代码简洁,可读性强,可提高代码的复用率。
  4. 缺点: 相较直接调用,在量大的情景下反射性能下降;存在一些内部暴露和安全隐患。

扩展阅读什么是反射?


【29】反射有哪些应用场景?

  1. JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;
  2. Eclispe、IDEA等开发工具利用反射动态解析对象的类型与结构,动态提示对象的属性和方法;
  3. Web服务器中利用反射调用了Sevlet的service方法;
  4. JDK动态代理底层依赖反射实现

【30】讲讲什么是泛型?

  1. 泛型是JDK5中引入的一个新特性,允许在定义类和接口的时候使用类型参数。声明的类型参数在使用时用具体的类型来替换。
  2. 泛型最大的好处就是可以提高代码的复用性。以List接口为例,我们可以将StringInteger等类型放入List中,如果不用泛型,存放String类型要写一个List接口,存放Integer要写另一个List接口,泛型可以很好的解决这个问题。

扩展阅读:什么是泛型?有什么作用?


【31】如何停止一个正在运行的线程?

  1. 使用线程的stop方法(该方法已被废弃);
    使用stop方法会一直向上传播ThreadDeath异常,从而使得目标线程解锁所有锁住的监视器,即释放掉所有的对象锁。使得之前被锁住的对象得不到同步的处理,因此可能会造成数据不一致的问题。

  2. 使用interrupt方法中断线程:
    该方法只是告诉线程要终止,但最终何时终止取决于计算机。调用interrupt方法仅仅是在当前线程中打了一个停止的标记,并不是真正的停止线程。

  3. 设置标志位:当标志位为某个值时,使线程正常退出。设置标志位是用到了共享变量的方式,为了保证共享变量在内存中的可见性,可以使用volatile修饰它,这样的话,变量取值始终会从主存中获取最新值。

    • 但是这种volatile标记共享变量的方式,在线程发生阻塞时是无法完成响应的。比如调用Thread.sleep() 方法之后,线程处于不可运行状态,即便是主线程修改了共享变量的值,该线程此时根本无法检查循环标志,所以也就无法实现线程中断。

因此,interrupt() 加上手动抛异常的方式是目前中断一个正在运行的线程最为正确的方式了。

扩展阅读:如何停止中断运行中的线程?


二、JVM篇


【1】JVM由哪些部分组成,运行流程是什么?
1.JVM主要由:类加载器、运行时数据区、执行引擎、本地库接口等组成。
2.运行流程:首先编译器将java源代码编译成.class字节码;类加载器将字节码加载到运行时数据区(加载到内存);执行引擎将字节码翻译为底层系统指令,再交由CPU执行,此时需要调用其他语言的本地库接口来实现整个程序的功能。

【2】什么是程序计数器?它的作用是什么?
程序计数器是线程私有的,内部保存字节码的行号,用于记录正在执行的字节码指令的地址。主要用于线程的上下文切换。

【3】介绍一下Java堆?
1.Java堆是线程共享的区域:主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展时,会抛出OutOfMemoryError异常。
2.JDK1.8后由年轻代和老年代组成:
年轻代:分为三个部分,Eden区和两个大小严格相等的Survivor区,根据JVM的策略,经过几次垃圾回收后,仍然存活于Survivor的对象将被移动到老年代区。
老年代区:主要用来保存生命周期长的对象。
元空间:用来保存类信息、静态变量、常量、编译后的代码。
3.JDK1.7和JDK1.8的区别:
JDK1.7中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码。
JDK1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出

【4.1】什么是虚拟机栈?
虚拟机栈是线程私有的,生命周期与线程相同,每个方法被执行的同时会创建栈帧。保存的是执行方法的局部变量、动态连接信息、操作数栈、方法返回地址信息等等。方法开始执行的时候会进栈,方法执行完出栈,不需要GC回收。

【4.2】垃圾回收是否涉及栈内存?
垃圾回收主要指的就是堆内存,不涉及栈内存。JVM栈当栈帧弹栈以后,内存就会自动释放。

【4.3】栈内存分配越大越好吗?
未必,默认的栈内存通常为1024K,栈内存过大会导致线程数变少

【4.4】方法内的局部变量是否线程安全?
1.如果方法内局部变量没有逃离方法的作用范围,它是线程安全的。
2.如果是局部变量引用了对象,并逃离方法的作用范围,则需要考虑线程安全。

【4.5】什么情况下会导致栈内存溢出?
1.栈帧过多会导致栈内存溢出,如递归调用。
2.栈帧过大会导致栈内存溢出,当栈内存分配完了已经没有多于空间,再去调用其他方法时,会出现OOM异常。
【4.6】堆和栈的区别是什么?
1.堆中存放的是对象实例和数组等,而栈内存一般会用来存储局部变量和方法调用,堆会GC垃圾回收,而栈不会。
2.栈内存是线程私有的,而堆内存是线程共享的。

【5.1】解释一下方法区?
方法区是各个线程共享的内存区域,主要存储类的信息、运行时常量池;虚拟机启动时创建,关闭虚拟机时释放;若方法区中的内存无法满足分配请求,则会抛出OutOfMemoryError:Metaspace。

【5.2】介绍一下运行时常量池
1.常量池可以看作一张表,虚拟机指令根据这张常量表来找到要执行的类名、方法名、参数类型、字面量等信息;
2.常量池是.class文件中的,当类被加载时,它的常量池信息就会放入运行时常量池中,并把里面的符号地址变为真实地址。

【6】什么是直接内存?
1.直接内存是一种特殊的内存缓冲区,并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存。
2.常见于NIO操作,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM内存回收管理。

【7.1】什么是类加载器?类加载器有哪些?
JVM只会执行二进制文件,而类加载器的主要作用就是将字节码加载到JVM中,从而让Java程序能够启动起来。
现有的类加载器基本上都是java.lang.ClassLoader的子类,该类的职责就是用于将指定的类找到或生成对应的字节码文件,同时类加载器还会负责加载程序所需要的资源
根据各自加载范围的不同划分为四个种类:
1.启动类加载器:用于加载JAVA_HOME/jre/lib目录下的类库[C++编写]
2.扩展类加载器:用于加载JAVA_HOME/jre/lib/ext目录中的类库[ClassLoader子类]
3.应用类加载器:用于加载classPath下的类,也就是加载开发者自己编写的java类[ClassLoader子类]
4.自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则

自定义类加载器–>>应用类加载器(自己写的程序在这儿)–>>扩展类加载器–>>启动类加载器

【7.2】什么是双亲委派模型?
在加载一个类时,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器才会尝试加载该类

【7.3】JVM为什么采用双亲委派机制?
1.为了防止类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
2.为了安全,保证类库API不会被修改

【8】说一下类加载的执行过程?
1.加载:查找和导入class文件
2.验证:保证加载类的准确性
3.准备:为类变量分配内存并设置类变量默认初始值
4.解析:把类中的符号引用转换为直接引用
5.初始化:对类的静态变量,静态代码块执行初始化操作
6.使用:JVM开始从入口方法开始执行用户的程序代码
7.卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象

【9】简述Java垃圾回收机制?
1.为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以在Java中有了自动垃圾回收机制,也就是GC。
2.不同的对象引用类型,GC会采用不同的回收时机。

【10】对象什么时候可以被垃圾回收器回收?
1.如果一个或多个对象没有任何引用指向它了,那么这个对象现在就是垃圾了,如果定位了垃圾,则有可能被垃圾回收器回收。
2.定位垃圾的方式有两种:引用计数法、可达性分析算法。

【11】JVM垃圾回收算法有哪些?
1.标记清除算法:垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
2.标记整理算法:标记算法同上,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
3.复制算法:将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低。

【12】说一下JVM中的分代回收算法
分代回收算法:
一、堆的区域划分

  1. 堆被分为两份,新生代和老年代【1:2】
  2. 新生代划分为三个区域,Eden区、Survivor区(From和To)【8:1:1】
    二、对象回收分代回收策略
  3. 新创建的对象都会分配到Eden区;
  4. 当Eden区内存不足,标记Eden区与From区的存活对象
  5. 将存活对象采用复制算法复制到To区,复制完毕后,释放Eden区和From区
  6. 经过一段时间后Eden区再次出现内存不足,这时标记Eden和To区的存活对象,复制到From区,清除Eden和To区的内存
  7. 当幸存区对象熬过几次回收(最多15次),就会晋升到老年区,或者当幸存区内存不足或大对象则会提前晋升

【13】MinorGC、MixedGC、FullGC的区别是什么?STW是什么?
1.MinorGC【YoungGC】:发生在新生代的垃圾回收,暂停时间短(STW)。
2.MixedGC:发生在新生代+老年代部分区域的垃圾回收,G1收集器持有。
3.FullGC:新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免。
STW:Stop-The-World,暂停所有应用程序线程,等待垃圾回收的完成。

【14】说一下JVM有哪些垃圾回收器?
1.串行垃圾回收器:
Serial GC:用于新生代,采用复制算法
Serial Old GC:用于老年代,采用标记-整理算法
2.并行垃圾回收器:
Parallel New GC:用于新生代,采用复制算法
Parallel Old GC:用于老年代,采用标记-整理算法
3.CMS(并发)垃圾回收器:
是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,以获取最短停顿时间为目的的收集器,在进行垃圾回收的过程中,应用仍能正常运行。
4.G1垃圾回收器,作用在新生代和老年代

【15】详细说一下G1垃圾回收器
G1垃圾回收器应用于新生代和老年代,在JDK9之后默认使用G1;其划分成多个区域,每个区域都可以充当Eden、Survivor、Old、Humongous区,其中Humongous区专为大对象准备;G1垃圾回收器采用复制算法;响应时间和吞吐量兼顾;主要分为三个阶段:
1.新生代回收(STW)
初始时,所有区域都处于空闲状态,创建一些对象后,挑出一些空闲区域作为Eden区来存储这些对象,当Eden区需要垃圾回收时,挑出一个空闲区域作为Survivor区,用复制算法复制存活对象到Survivor区,并释放Eden区内存,此时需要暂停用户线程(STW)。随着时间流逝,Eden区的内存又不足时,将Eden区以及之前Survivor区中的存活对象采用复制算法复制到新的Survivor区,其中较老对象晋升到老年代。
2.并发标记(重新标记时STW)
当老年代占用内存超过阈值(45%)之后会触发并发标记,这时无需暂停用户线程,但是并发标记之后会又重新标记阶段解决漏标问题,此时需要暂停用户线程,这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段,根据暂停时间目标优先回收价值高(也就是存活对象少)的区域。
3.混合垃圾回收阶段
复制完成,内存得到释放,进入下一轮的新生代回收、并发标记、混合回收。如果并发失败(即回收速度赶不上创建新对象的速度),会触发FullGC。

【16】强引用、软引用、弱引用、虚引用的区别?
强引用:只要所有GC Roots能找到,就不会被回收。
软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够时会回收软引用对象。
弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收。
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler。线程调用虚引用相关方法释放直接内存。
【17】JVM调优的参数可以在哪里设置参数值?
1.war包部署在tomcat中设置:修改TOMCAT_HOME/bin/catalina.sh文件。
2.jar包部署在启动参数设置:java -Xms512m -Xmx1024m -jar xxxx.jar。

【18】用的JVM调优的参数都有哪些?
1.设置堆空间大小。
2.虚拟机栈大小的设置。
3.年轻代中Eden区和两个Survivor区的大小比例。
4.年轻代晋升老年代阈值。
5.设置垃圾回收器。

【19】说一下JVM调优工具?
命令工具:
1.jps:进程状态信息。
2.jstack:查看java进程内线程的堆栈信息。
3.jmap:查看堆转信息。
4.jhat:堆转储快照分析工具。
5.jstat:JVM统计监测工具。
可视化工具:
1.jconsole:用于对JVM的内存,线程,类的监控。
2.VisualVM:能够监控线程,内存情况。

【20】Java内存泄漏的排查思路?
内存泄漏通常指堆内存中一些大对象不被回收的情况,故障排查思路:
1.通过jmap或设置jvm参数获取堆内存快照dump。
2.通过工具,VisualVM去分析dump文件,ViualVM可以加载离线的dump文件。
3.通过查看堆信息的情况,可以大概定位内存溢出是哪行出现了问题。
4.找到对应的代码,通过阅读上下文的情况,进行修复即可。

【21】CPU飙高排查方案与思路?
1.使用top命令查看占用CPU的情况。
2.通过top命令查看后,可以查看是哪个进程占用CPU较高。
3.使用ps命令查看进程中的线程信息。
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题。

JUC并发
【1】并发和并行有什么区别?
1.并发是两个及两个以上的作业在同一时间段内被执行
2.并行是两个及两个以上的任务在同一时刻被执行
最关键的点在于时间点,是否是同时执行

【2】线程和进程的区别?
1.进程是正在运行的程序的实例,进程中包含了线程,每个线程执行不同的任务
2.不同的进程使用不同的内存空间,同一进程下的线程共享内存空间
3.线程的上下文切换成本一般比进程低,线程更轻量

【3】程序计数器为什么是私有的?
线程的程序计数器私有主要是为了线程切换后能够恢复到正确的执行位置

【4】虚拟机栈和本地方法栈为什么是私有的?
为了保证线程中的局部变量不被其他线程访问到

【5】同步和异步的区别?
同步是发布一个调用后,在没有得到结果返回之前,该调用就不能够返回,一直等待。
异步是调用发出之后,不用等待返回结果,该调用直接返回。

【6】使用多线程可能带来什么问题?
内存泄漏、死锁、线程不安全等问题

【7】如何理解线程安全和不安全?
线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性
1.线程不安全:即在多线程访问下,同一份数据可能会产生数据混乱、错误或者丢失问题
2.线程安全:即在多线程访问下,能保证同一份数据的正确性和一致性

【8】单核CPU下运行多个线程效率一定高吗?
这得分情况,在CPU密集型任务下,多线程运行会导致来回的线程切换降低效率;而在I/O密集型任务下,多线程运行可以利用CPU在等待IO时的空闲时间,提高效率。

【9】如何创建线程?
1.继承Thread类
2.实现Runnable接口
3.实现Callable接口
4.线程池创建线程

【10】runnable和callable两个接口创建线程有什么不同?
1.Runnable接口的run方法无返回值,而Callable的call方法有返回值,是个泛型,可以配合Future、FutureTask来获取异步执行的结果,使用FutureTask.get()方法。
2.他们的异常处理也不一样,Runnable接口run方法只能抛出异常,也无法捕获异常;而Callable的call方法允许抛出异常,可以获取异常信息。

【11】线程包括哪些状态?状态之间是如何变化的?
1.Java线程有6种状态:分别是新建(new)、可运行(runnable)、终止(terminated)、阻塞(blocked)、等待(waiting)、有时限等待(time_waiting)。
2.当一个线程被创建,但还未执行start方法时处于新建状态,调用了start方法后,会由新建状态转为可运行状态。如果线程内的代码执行完,则由可运行状态转为终结状态。当然这是一个线程正常执行的情况。
3.如果线程获取锁失败,则会由可运行状态进入阻塞状态,会将该线程加入到Monitor的阻塞队列阻塞。只有当持锁线程释放后,会按照一定的规则唤醒阻塞队列种的阻塞线程,唤醒后的线程进入可运行状态。
4.如果线程获取锁成功后,但由于条件不满足,调用了wait方法,则会由可运行状态转为等待状态,当其他持锁线程调用notify或notifyAll方法后会恢复成可运行状态。
5.还有一种情况是调用了sleep方法,也会从可运行状态转为有时限等待状态,该状态不需要唤醒,当然也可以被唤醒,超时就会自然恢复为可运行状态。

【12】线程中的wait和sleep方法有什么不同呢?
相同点是:两个方法都可以让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点:
1.方法归属不同:sleep是thread的静态方法,而wait是object的成员方法,每个对象都有
2.线程醒来时机不同:wait需要由notify通知唤醒,而sleep可以根据参数来指定醒来时机
3.锁特性不同:wait执行后会释放锁,而sleep执行后并不会释放锁

【13】新建T1、T2、T3,如何保证他们的顺序性
可以用线程类的join方法,在一个线程中启动另一个线程,另一个线程完成后,该线程继续执行。
比如:使用join方法,T3调用T2,T2调用T1,这样就能保证T1就会先完成而T3最后完成

【14】现成的run()和start()有什么区别?
1.调用start方法方可启动线程并使线程处于就绪状态,而直接执行run方法并不会以多线程的方式执行
2.start方法用来启动线程,通过该线程自动调用run方法执行run方法种定义的逻辑代码。start方法只能被调用一次。run方法封装了要被线程执行的代码,可以被调用多次。

【15】如何停止一个正在运行的线程?
1.使用退出标志
2.使用stop方法
3.使用interrupt方法中断线程

【16】什么是线程上下文切换?
线程在执行过程中有自己的运行条件和状态,比如程序计数器和栈信息等。当出现以下情况时,线程会从占用CPU状态中退出:
1.线程主动放弃占有CPU:如调用了wait和sleep方法
2.线程调用了阻塞类型的系统中断,比如IO请求,线程被阻塞
3.时间片用完
4.线程被终止或结束运行
前三种情况会发生线程上下文切换,线程切换需要保存当前的运行上下文,留待下一次占用cpu时恢复现场,并加载下一个需要运行的线程的上下文。这就是上下文切换。
由于上下文切换需要保存信息和恢复信息,会占用CPU和内存等系统资源进行处理,因此频繁的上下文切换会导致系统效率变低。

【17】什么是线程死锁?产生死锁的条件?怎么预防和解决?
死锁是多个线程同时被阻塞,他们中的一个或多个都在等待某个资源的释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
死锁产生有四个必要条件:分别是互斥条件、请求并保持条件、不剥夺条件、循环等待条件
1.互斥条件:该资源任意时刻只能由一个线程占有。(一次性申请所有的资源)
2.请求并保持条件:一个线程因请求资源被阻塞时,其占有的资源保持不放。
3.不剥夺条件:线程执行完任务前,其占有的资源不能被其他线程强行剥夺。(若申请不到资源,则释放已有资源)
4.循环等待条件:若干线程形成一种头尾相接的循环等待资源关系。(按顺序申请资源)
避免死锁:采用算法(银行家算法)对每次资源发放进行一个安全性评估,若是安全的才分配资源给线程。

【18】讲一下synchronized关键字?以及其底层原理?
作用:synchronized是Java的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只有一个线程执行。
原理:synchronized底层使用的是JVM级别的Monitor来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放之前,其他线程是不能获得锁的,synchronized属于悲观锁。由于使用到了JVM级别的Monitor,因此Synchronized的性能相对较低。

【19】具体说一下Monitor
1.Monitor对象存在于每个java对象的对象头中。monitor内部维护了三个变量:waitset、entrylist、owner。
2.其中waitset保存处于waiting状态的线程、entrylist保存处于blocked状态的线程、owner保存持有锁的线程。
3.只有一个线程获取到的标志就是在monitor中设置成功了owner,一个monitor中只能由一个owner。在上锁的过程中,若有其他的线程也来抢锁,则会进入entrylist进行阻塞,当持锁线程执行完后,释放了锁,这时就会唤醒entrylist中等待的线程来竞争锁,竞争的时候是非公平的。

【20】关于synchronized锁升级的情况了解吗?
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程所持有、不同线程交替持有锁、多线程竞争锁三种情况。
1.偏向锁:在第一次获取锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
2.轻量级锁:轻量级锁修改了对象头的锁标志,相对于重量级锁性能提升了很多,每次修改都是CAS操作,保证原子性
3.重量级锁:底层使用monitor实现,里面涉及到了用户态和内核态的切换、进程上下文切换,成本较高,性能较低。
[只要发生了锁竞争,都会升级为重量级锁]

【21】Synchronized在高并发量的情况下,性能不高如何解决?
可以采用ReentrantLock来加锁。
1.ReentrantLock是一个可重入锁:通过调用lock()方法获取了锁以后,再次调用lock()方法不会被阻塞,内部直接增加重入次数就行了。它属于JUC包下的类,属于api层面的锁,跟Synchronized一样都是悲观锁。通过lock()获取锁,unlock()释放锁。
2.其底层实现原理主要利用CAS+AQS队列来实现。支持非公平锁和公平锁,默认是非公平锁,可以在构造方法传true变为公平锁。公平锁的效率往往没有非公平锁的效率高。

【22】Synchronized和ReentrantLock的区别?
1.两者都是可重入锁。
2.synchronized依赖于JVM,而ReentrantLock依赖于API。
3.ReentrantLock相对于Synchronized来说增加了一些高级功能:等待可中断、可实现公平锁、可实现选择性通知。

【23】可中断锁与不可中断锁有什么区别?
1.可中断锁:获取锁的过程中可以被中断,不需要一直等待到获取锁之后才进行其他逻辑处理。ReentrantLock属于可中断锁。
2.不可中断锁:获取锁的过程中不可被中断,只要申请获取锁,就无法做其他事情,直到获取成功。synchronized就是不可中断锁。

【24】什么是乐观锁和悲观锁?
1.悲观锁:悲观锁总是假设最坏情况,认为共享资源每次访问都会出现问题,所以每次访问之前都会上锁,直到访问完毕释放完锁,其他线程才可以进行访问。即[共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程]
2.悲观锁存在的问题:高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。
3.乐观锁:乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交的时候去验证对应的资源是否被其他线程修改。[版本号机制或者CAS算法]

应用场景:悲观锁(多写场景,竞争激烈) 乐观锁(多读场景,竞争较少)

【25】什么是CAS算法?
1.CAS全称Compare and Swap(比较再交换);它体现的是一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
2.CAS使用场景:AQS框架、AtomicXXX类。
3.优点:在操作共享变量的时候使用自旋锁,效率更高
4.CAS底层调用的是Unsafe类中的方法,都是操作系统提供的,其他语言实现。

【26】什么是AQS?
AQS其实是jdk提供的类AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
其内部有一个state属性来表示资源的状态,初始为0,表示没有获取锁,当state等于1的时候标明获取到了锁,通过cas算法设置state的状态。当有线程要获取锁时,cas操作state若发现state为1则进入到AQS维护的一个FIFO等待队列,这个队列是一个双向列表,tail指向最后一个元素,head指向队列中最久的一个元素,当state=0时,会取head指向的线程来竞争锁,但是是非公平竞争。

【27】乐观锁存在哪些问题?乐观锁的实现?
1.ABA问题。
2.循环时间长开销大(CAS自旋操作)。
3.只能保证一个共享变量的原子操作。
乐观锁实现:CAS算法和版本号机制。

【28】Synchronized和Lock的区别?
1.语法层面:S是关键字,源码在jvm中,用c++实现,退出同步代码块会自动释放锁。而L是接口,源码由jdk提供,用java实现,需要手动调用unlock()释放锁。
2.功能层面:两者都属于悲观锁,都具备基本的互斥、锁重入、同步功能。L提供了许多S不具备的功能,如获取等待状态、公平锁、可打断、可超时、多条件变量,同时L可以实现不同的场景,如ReentrantLock和ReentrantReadWriteLock。
3.性能层面:S在没有激烈竞争的情况下,做了很多优化,如偏向锁、轻量级锁,性能高
在竞争激烈的情况下,L的实现通常会提供更好的性能。

【29】请你谈谈对volatile的理解
1.volatile是一个关键字,可以修饰类的成员变量、类的静态成员变量。
2.保证不同线程对volatile修饰的变量的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存。
3.禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

【30】线程池的种类有哪些?
jdk中默认提供了4种方式创建线程池
1.newCachedThreadPool:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
2.newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3.newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
4.newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。

【31】线程池的核心参数有哪些?
1.corePoolSize(核心线程数)
2.maximumPoolSize(最大线程数):核心线程+救急线程的最大数目。
3.keepAliveTime(生存时间):救急线程的生存时间,生存时间内没有新任务,此线程资源会释放。
4.unit(生存时间单位):救急线程的生存时间单位,如秒、毫秒等。
5.workQueue(工作队列):当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务。
6.threadFactory(线程工厂):可以定制线程对象的创建,例如设置线程名字、是否是守护线程等。
7.handler(拒绝策略): 当所有线程都在繁忙,workQueue也放满时,会触发拒绝策略。

【32】如何确定核心线程池呢?
为了减小上下文切换,根据部署的服务器的cpu核数来确定,如CPU核数+1。

【33】线程池的执行原理
首先判断线程池里的核心线程是否都在工作,如果不是,则创建一个新的工作线程来执行任务。如果核心线程都在工作,则线程池判断工作队列是否已满,如果工作队列没有满,则将任务放到工作队列中。如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建新线程来执行任务,否则交给拒绝策略来处理任务。
Java集合
【1】说说List、Set、Queue、Map的区别?
1.List:存储的元素是有序的、可重复的;
2.Set:存储的元素是不可重复的;
3.Queue:按特定的排队规则来确定元素的先后顺序,存储的
元素是有序的、可重复的;
4.Map:使用键-值对存储,key是无序的、不可重复的,value
是有序的,可重复的,每个键最多映射到一个值。

【2】集合框架底层数据结构总结。

1.Collection接口下面的集合
(1)List:

  • ArrayList:数组;
  • Vector:数组;
  • LinkedList:双向链表;
    (2)Set:
  • HashSet(无序,唯一):基于 HashMap实现,底层采用HashMap保
    存元素;
  • LinkedHashSet:HashSet的子类,内部通过LinkedHashMap实
    现;
  • TreeSet(有序,唯一):红黑树
    (3)Queue:
  • PriorityQueue:数组来实现小顶堆;
  • DelayQueue:PriorityQueue;
  • ArrayDeque:可扩容动态双向数组。
    2.Map接口下的集合
    (1)HashMap:jdk1.8之前由数组+链表组成;jdk1.8之后,当链表长度
    大于阈值,会将链表转化为红黑树,减少搜索空间;
    (2)LinkedHashMap:继承自HashMap,底层仍然是基于拉链式散列结构
    即由数组和链表或红黑树组成。此外,在上面结构
    的基础上,增加了一条双向链表,使得上面的结构
    可以保持键值对的插入顺序,实现了访问顺序相关
    逻辑;
    (3)Hashtable:数组+链表组成,数组是主体,链表是为了解决哈希冲
    突而存在的;
    (4)TreeMap:红黑树。

【3】如何选用集合?
主要是根据集合的特点来进行选用:
1.如果需要根据键值获取元素值时就选用Map接口下的集合;需要排序时选择TreeMap;不需要排序时选择HashMap;需要保证线程安全就选择ConcurrentHashMap。
2.若只需要存放元素值,就选择Collection接口下的集合,需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet;不需要就选择List接口下的集合,比如ArrayList或LinkedList,然后再根据实现这些接口的集合的特点来选用。

【4】ArrayList和Array(数组)的区别?
ArrayList内部基于动态数组实现,比Array(静态数组)使用起来更加灵活:
1.ArrayList会根据时机存储的元素动态的扩容或缩容,而Array被创建之后就不能改变它的长度了。
2.ArrayList允许使用泛型来确保类型安全,而Array不可以。。
3.ArrayList中只能存储对象。对于基本类型数据需要使用对应的包装类,Array可以直接存储基本类型数据,也可以存储对象。
4.ArrayList支持插入、删除、遍历等常见操作,并且提供了丰富的API操作方法,比如add、remove等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
5.ArrayList创建时不需要指定大小,而Array创建时必须指定大小。

【5】ArrayList与LinkedList的区别?
1.是否保证线程安全:两者都不保证线程安全;
2.底层数据结构:ArrayList(数组);LinkedList(双向链表);
3.插入和删除是否受元素位置影响:ArrayList受元素位置影响;LinkedList在头尾处插入和删除不受元素位置影响;
4.是否支持快速随机访问:ArrayList支持(实现了RandomAccess接口);LinkedList不支持高效的随机元素访问;
5.内存占用空间:ArrayList的空间主要浪费在list列表的结尾会预留一定的容量;而LinkedList的空间花费体现在它的每一个元素都要消耗更多的空间(因为要存放后继和直接前驱指针及元素)。

【6】ArrayList底层的实现原理是什么?
1.ArrayList底层是用动态数组实现的;
2.ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10;
3.ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组;
4.ArrayList在添加数据的时候:

  • 确保数组已使用长度(size)加1之后足够存下下一个数据;
  • 计算数组的容量,如果当前数组已使用长度+1后大于当前数组长
    度,则调用grow方法扩容(原来的1.5倍);
  • 确保新增的数据有地方存储之后,则将新元素添加到位于size的
    位置上;
  • 返回添加成功布尔值;

【7】ArrayList list = new ArrayList(10);中的list扩容了几次?
该语句只是声明和实例了一个ArrayList,指定了容量为10,未扩容。

【8】如何实现数组和List之间的转换?
1.数组转list,使用JDK中java.util.Arrays工具类的asList方法
2.List转数组,使用List的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组。
用Arrays.asList转为List后,如果修改了数组内容,list受影响吗?
数组转List受影响。
List用toArray转数组后,如果修改了List内容,数组受影响吗?
List转数组不受影响。

【9】无序性和不可重复性的含义是什么?
1.无序性不等于随机性,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。
2.不可重复性是指添加的元素按照equals()判断时,返回false,需要同时重写equals()方法和hashCode()方法。

【10】比较HashSet、LinkedHashSet、TreeSet三者的异同
1.HashSet、LinkedHashSet和TreeSet都是Set接口的实现类,都能保证元素唯一,并且都不是线程安全的;
2.HashSet、LinkedHashSet和TreeSet的主要区别在于底层数据结构不同:
HashSet:哈希表;
LinkedHashSet:链表+哈希表,元素的插入和取出顺序满足FIFO;
TreeSet:红黑树,元素是有序的,排序的方式有自然排序和定制排序
3.底层数据结构不同导致这三者的应用场景不同。
HashSet:用于不需要保证元素插入和取出顺序的场景;
LinkedHashSet:用于保证元素的插入和取出顺序满足FIFO;
TreeSet:用于支持对元素自定义排序规则的场景。

【11】ArrayDeque与LinkedList的区别?
ArrayDeque和LinkedList都实现了Deque接口,两者都具备队列的功能,两者区别:
1.ArrayDeque是基于可变长数组和双指针实现的,而LinkedList则通过链表来实现。
2.ArrayDeque不支持存储NULL数据,但LinkedList支持。
3.ArrayDeque插入时可能存在扩容过程,不过均摊后插入操作依然为O(1)。虽然LinkedList不需要扩容,但每次插入数据时都需要申请新的对空间,均摊性能相比更慢。

【12】说一说PriorityQueue
1.PriorityQueue是在jdk1.5中被引入的,其与Queue的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。
2.PriorityQueue利用了二叉堆的数据结构来实现的,底层使用可变长数组来存储数据;通过堆元素的上浮和下沉,实现了在O(logn)的时间复杂度内插入和删除堆顶元素。是非线程安全的,且不支持存储NULL和non-comparable的对象。默认是小顶堆,但可以接收一个Comparator作为构造参数,从而来自定义元素优先级的先后。

【13】说一下HashMap的实现原理
1.底层使用hash表数据结构,即数组+(链表或红黑树)。
2.添加数据时,计算key的值确定元素在数组中的下标。

  • key相同则替换
    -不同则存入链表或红黑树中
    3.获取数据通过key的hash计算数组下标获取元素。

【14】HashMap的jdk1.7和jdk1.8有什么区别?
1.JDK1.8之前,HashMap采用的是数组+链表实现的。
2.JDK1.8以后,HashMap采用的是数组+链表+红黑树实现的;当链表长度大于8且数组长度大于64则会从链表转化为红黑树。

【15】HashMap的put方法的具体流程。
1.首先判断table是否为空,若空,则执行resize()方法进行初始化,默认初始化长度为16的数组。
2.若table不空,则根据key计算hash值,接着判断table[i]是否为null,若是则直接插入,否则判断key是否存在,若存在则直接覆盖。
3.若不存在则判断是否为红黑树。
3.1 若是则直接插入数据到红黑树,然后判断++size是否大于threshold,若是则执行resize()进行扩容,否则结束。
3.2 否则遍历链表,看key是否存在,若存在则直接覆盖,否则在链表尾部插入节点,接着判断链表长度是否大于8,若是则转为红黑树,否则直接插入,最后也判断以下++size>threshold,看下是否需要扩容。

【16】HashMap扩容机制
1.添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度*0.75)。
2.每次扩容的时候,都是扩容之前容量的2倍。
3.扩容之后,会创建一个新的数组,需要把老数组中的数据挪动到新的数组中。
3.1 没有hash冲突的节点,直接使用e.hash & (newCap - 1)计算新数组的索引位置。
3.2 如果是红黑树,走红黑树的添加。
3.3 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置。

【17】HashMap的寻址算法
1.计算对象的hashCode()。
2.再进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更均匀。
3.最后(capacity - 1) & hash得到索引。

【18】为何HashMap的数组长度一定是2的次幂?
1.计算索引时效率更高:如果是2的n次幂,则可以使用位与运算代替取模。
2.扩容时重新计算索引效率更高:hash & oldCap == 0的元素留在原来的位置,否则新位置=旧位置+oldCap。

MySQL篇
【1】MySQL中如何定位慢查询?
1.可以采用运维的监控系统Skywalking,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体执行时间,所以可以定位是哪条SQL出了问题
2.MySQL的系统配置文件中可以开启慢日志查询的功能,并且可以设置SQL执行超过了多少时间来记录到一个日志文件中,比如设置为2秒,那么当SQL执行时间超过2秒就会记录到日志文件中,然后就可以在日志文件找到比较慢的SQL了

【2】SQL语句执行慢如何分析?
若有执行慢的SQL语句,可以采用如下方法分析
1.使用MySQL自动的执行计划explain来查看这条SQL的执行情况,比如可以通过key和key_len检查是否命中了索引,若本身添加了索引,也可以判断索引是否有失效的情况。
2.通过type字段查看sql是否有进一步的优化空间,是否存在全盘扫描或全索引扫描。
3.通过extra建议来判断是否出现回表的情况,若出现了,可以尝试添加索引或修改返回字段来修复。

【3】了解过索引吗?(什么是索引?)
索引是帮助MySQL高效获取数据的数据结构;主要是用来(1)提高数据检索的效率;(2)降低数据库的IO成本;(3)通过索引列对数据进行排序,降低数据排序的成本,能够降低CPU的消耗

【4】索引的底层数据结构了解过吗?
MySQL的默认引擎InnoDB采用B+树作为索引的数据结构,选择B+树的主要原因有:
(1)阶数更多,路径更短。
(2)磁盘读写代价更低,非叶子节点只存指针,只有叶子节点才存数据。
(3)B+树便于扫库和区间查询,叶子节点是一个双向链表。

【5】B树和B+树的区别是什么?
(1)B树非叶子节点和叶子节点都会存放数据,而B+树仅在叶子节点存放数据,非叶子节点只存放指针,在查询的时候,B+树查找效率更稳定。
(2)进行范围查询的时候,B+树的效率更高,因为B+树只在叶子节点存放数据,并且叶子节点是一个双向链表。

【6】什么是聚簇索引?什么是非聚簇索引?
(1)聚簇索引:指数据与索引放到一块儿,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下以主键作为聚簇索引。
(2)非聚簇索引:指数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般自定义的索引都是非聚簇索引。

【7】什么是回表查询?
回表的意思是通过二级索引找到对应的主键值,再通过主键值找到聚簇索引中对应的整行数据,这个过程就是回表。
【8】什么是覆盖索引?
(1)覆盖索引指的是select查询语句使用了索引,在返回的列中,必须在索引中全部都能够找到,这样就不用进行回表查询,一次找到所有数据。
(2)如果使用id查询,它会直接走聚簇索引查询,一次索引扫描,就能直接返回数据,性能高。
若按照二级索引查询数据,返回的列中没有创建索引,有可能会触发回表查询。应尽量避免使用select *,尽量在返回的列中都包含添加索引的字段。
[另一种解释:sql查询完全命中索引时称为覆盖索引]

【9】MySQL超大分页怎么处理?
采用覆盖索引和子查询来解决:
1.超大分页一般都是在数据量比较大时,使用limit分页查询需要对数据进行排序,这个时候效率就比较低了。先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表的数据就可以了。因为查询id的时候,走的是覆盖索引,所以效率可以提升很多。
[扩展]:limit 10000,10的语法实际上是mysql查找到前10010条数据,之后丢弃前面的10000行,这个步骤其实是浪费掉的。

【10】索引的创建原则有哪些?
有一个大前提就是表中的数据要超过10万以上,才会创建索引。
(1)添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件、排序字段、分组字段这些。
(2)使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,也会把它放在复合索引后面的字段。
(3)若某一字段内容过长,应使用前缀索引。
(4)当然并不是所有的字段都要创建索引,索引的数量需要控制,因为添加索引会导致新增和修改的速度变慢。

【11】什么情况下索引会失效?
1.违反最左匹配法则
2.范围查询右边的列不能使用索引
3.在索引列上进行运算操作也会导致索引失效
4.字符串不加单引号会进行类型转换,进而导致索引失效
5.以%开头的模糊查询会导致索引失效
[判断索引失效:explain执行计划]

【12】SQL的优化经验?
1.表的设计优化,数据类型的选择。
2.索引优化,依据索引创建原则创建索引。
3.SQL语句优化,避免索引失效,避免使用select *。
4.主从复制、读写分离、不让数据的写入影响读。
5.当数据量大时,可以考虑分库分表。

【13】创建表的时候,你们是如何优化的呢?
1.定义字段的时候,结合字段的内容来选择合适的类型。
2.如果是数值的话,根据实际情况选择tinyint、int、bigint。
3.如果是字符串的话,也结合存储的内容来选择char、varchar或text类型

【14】使用索引的时候是如何优化的?
1.对查询比较频繁的字段创建索引,如查询条件、排序字段、分组等字段;
2.创建索引的时候采用复合索引来创建,一条sql的返回值尽量使用覆盖索引。
3.若字段过长,则使用前缀索引。
4.控制索引数量,添加索引会导致新增和修改速度变慢。

【15】你平时对sql语句做了哪些优化呢?
1.使用select语句时务必指明字段名称,不要直接使用select *;
2.注意sql语句避免造成索引失效情况;
3.聚合查询时尽量用union all代替union,因为union会多一次过滤,效率比较低;
4.如果是表关联的话,尽量使用inner join,而不是left join,right join,如果必须使用,一定要以小表驱动。

【16】事务的特性是什么?可以详细说一下吗?
事务的特性ACID:原子性、一致性、隔离性、持久性;
1.原子性:要么都成功,要么都失败;
2.一致性:事务提交后数据要一致;
3.隔离性:事务进行时,不受其他事务干扰;
4.持久性:事务提交后,要做数据的持久化。

【17】并发事务带来哪些问题?
并发事务问题:脏读、不可重复读、幻读
隔离级别:读未提交、读已提交、可重复读、串行化
1.脏读(一个事务读到了另一个事务还没有提交的数据):当一个事务正在访问数据并对数据进行了修改,而这种修改还没有提交到数据库中时,又来了另外一个事务也访问了这个数据,而该数据还没有提交完成,导致读到的数据是未修改成功的,因此读到了脏数据,而根据这个脏数据来进行后续的操作可能是不正确的;
2.不可重复读(一个事务先后读取到同一个数据,但两次读取的数据不同):
当一个事务读一个数据时,第一次读到数据后比如读到了数字3,此时另外一个事务对该数据进行了修改,改成了5,然后第一个事务再次读该数据是发现读到的是5,两次读操作读到的数据不一致,因此称为不可重复读;
3.幻读(一个事务查询数据时,没有对应的数据行,但在插入数据时,又发现这行数据已存在):
比方说事务T1读取了几行数据,接着另一个并发事务T2插入了一些数据时。在随后的查询中,T1就会发现多了一些原本不存在的数据,就好像发生了幻觉一样,因此被称为幻读。

【18】怎么解决并发事务带来的问题呢?
MySQL支持4中隔离级别,分别有:
1.读未提交:它解决不了刚才提出的所有问题,一般项目中也不用这个
2.读已提交:能够解决脏读,但解决不了不可重复读和幻读问题
3.可重复读:能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别。
4.串行化:它可以解决所有并发事务带来的问题,但是由于是让事务串行执行,性能比较低。所以一般是使用mysql默认的隔离级别:可重复读。

【19】undo log和redo log的区别?
1.undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条delete语句,如果发生回滚就执行逆操作;
2.redo log:记录数据页的物理变化,服务宕机时可用来同步数据;
[redo log保证了事务的持久性;undo log保证了事务的原子性和一致性;]

【20】事务中的隔离性是如何保证的呢?(=解释一下MVCC)
1.事务的隔离性是由锁和MVCC实现的。
2.其中MVCC是多版本并发控制。指维护了一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图。

【21】MySQL主从同步的原理
MySQL主从复制的核心就是二进制日志,它的步骤如下:
(1)主库在事务提交时,会把数据变更记录在二进制日志文件BinLog中。
(2)从库读取主库的二进制日志文件Binlog,写入到从库的中继日志RelayLog。
(3)从库重做中继日志中的事件,将改变反映它自己的数据

【22】你们项目用过 MySQL的分库分表吗?
采用微服务开发时,每个服务对应一个数据库,根据业务进行拆分,也就是垂直分库

【23】之前使用过水平分库吗?
当业务量上来后,数据量变大时,采用水平分库:
1.一开始先做3台服务器对应3个数据库,由于库多了,需要分片,采用mycat作为数据库的中间件。数据都是按照id(自增)取模的方式来存取。
2.一开始那些旧数据做了清洗工作,按照id取模规则分别存储到各个数据库中,好处就是可以让各个数据库分摊存储和读取压力。

Redis篇
【1】什么是缓存穿透?怎么解决?
(1)缓存穿透:查询一个一定不存在的数据,因为该数据不存在,故在存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,可能导致DB挂掉,这种情况大概率是招到了攻击。
(2)[解决方案]:1.非法请求的限制 2.设置空值或者默认值 3.采用布隆过滤器。
(3)布隆过滤器:主要用于检索一个元素是否在一个集合中。可以使用redission实现布隆过滤器。其底层主要是先去初始化一个比较大的数组,里面存放的是二进制的0或1。一开始都为0,当一个key来之后经过3次hash计算,模于数组长度找到数据的下标,然后把数组中原来的0改为1,这样的话三个数组的位置就能表明一个key是否存在,查找过程相同。

【2】什么是缓存击穿?怎么解决?
缓存击穿:指的是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求,这些请求发现缓存过期就会去访问DB来加载数据并回设到缓存中,这个时候大量的并发请求可能瞬间把DB压垮。
解决方案:
1.采用互斥锁:缓存失效时,不去立即load db,先使用Redis的setnx去设置一个互斥锁,然后再去load db并回设缓存,否则重试get缓存的方法
2.采用key的逻辑过期:在存key时设置一个过期时间字段一起存进缓存中,不给当前key设置过期时间;取key时,从redis取出数据后判断数据是否过期;若过期则开通另外一条线程来进行数据同步,当前线程正常返回数据,当然这个数据不是最新的。
[第一种方案可以做到强一致,但性能不如第二种方案,但是第二种方案是高可用的。]

【3】什么是缓存雪崩?怎么解决?
缓存雪崩:指的是设置缓存时采用了相同的过期时间,导致某一时刻同时失效,这时对于这些key的请求全部转发到DB,DB瞬时压力过重雪崩。
[与缓存击穿的区别:雪崩是多个key失效;击穿是一个key失效。]
解决方案:
1.将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。
2.设置缓存不过期:通过后台服务来更新缓存。

【4】redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
采用redission实现的读写锁。在读的时候添加共享锁,可以保证读不互斥,读写互斥。当更新数据时,添加排他锁,读写互斥并且读读互斥,这样能保证在写数据的同时不会让其他线程读数据,避免脏数据。读与写需上同一把锁。

【5】排他锁是如何实现读写、读读互斥呢?
排他锁底层使用的也是setNX,保证了同一时刻只能有一个线程操作锁住的方法
【6】听说过延时双删吗?为什么不用它呢?
延时双删:如果是写操作,先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
【7】Redis作为缓存,数据的持久化是怎么做的?
1.RDB快照文件
2.AOF日志文件

【8】这两种持久化方式有什么区别?
1.RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据时,方便从RDB快照文件中恢复数据。
2.AOF的含义是追加文件,当redis操作写命令时,都会存储一份命令追加到AOF日志文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。

【9】这两种方式,哪种恢复的比较快?
RDB更快,因为RDB快照是二进制文件,在保存的时候体积也比较小,它恢复的比较快,但是它有可能会丢失数据。通常采用AOF来恢复数据,虽然AOF慢一些,但是它丢失数据的风险要小很多,在AOF文件中可以设置刷盘策略,always、everysec、no。

【10】Redis的数据过期策略有哪些?
Redis提供了两种数据过期删除策略
1.惰性删除:给key设置过期时间后,不去管它,当需要该key时,再检查其是否过期,若过期就删除掉它,否则返回该key。
2.定期删除:每隔一段时间,就对一些key进行检查,删除里面过期的key
(1)SLOW模式:定时任务,执行频率默认为10hz,每次不超过25ms,可以通过修改redis.conf来修改hz选项调整次数
(2)FAST模式:执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms
[Redis过期删除策略:惰性删除+定期删除结合使用]

【11】Redis数据淘汰策略有哪些?
1.默认noeviction:不删除任何数据,内存不足直接报错
2.在设置了过期时间的数据中进行淘汰:
(1)volatile-random:随机淘汰设置了过期时间的任意键值;
(2)volatile-ttl:优先淘汰更早过期的键值。
(3)volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值。
(4)volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值
3. 在所有数据范围内进行淘汰:
(1)allkeys-random:随机淘汰任意键值;
(2)allkeys-lru:淘汰整个键值中最久未使用的键值;
(3)allkeys-lfu:淘汰整个键值中最少使用的键值
[LRU:最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
LFU:最少频率使用,会统计每个key的访问频率,值越小淘汰优先级越高]

【12】数据库中有1000万数据,Redis只能缓存20W数据,如何保证Redis中的数据都是热点数据?
可以使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据
【13】Redis中的内存用完了会发生什么?
这个要看Redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后会直接报错。

【14】Redis分布式锁如何实现?
1.在Redis中提供了一个命令setnx(SET if not exist),即如果key不存在,才会设置它的值,否则什么都不做。
2.由于redis是单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候,其他客户端是不能设置这个key值的,因此实现了分布式锁。

【15】如何控制Redis实现分布式锁的有效时长呢?
redis的setnx指令不好控制这个问题,可以采用redission实现:
1.在redission中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redission中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有,就增加加锁的持有时间,当业务执行完成之后需要使用时,执行释放锁就可以了。

【16】redission实现的分布式锁是可重入的吗?
1.是可重入的;这样做是为了避免死锁的产生;这个重入其实是在内部判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。
2.存储数据时采用hash结构,key是当前线程的唯一标识,value是当前线程重入的次数。

【17】redission实现的分布式锁能解决主从一致性问题吗?
不能解决,但是可以使用redission提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议使用zookeeper实现的分布式锁

【18】redis集群有哪些方案,知道吗?
redis中提供的集群方案总共有三种:主从复制、哨兵模式、分片集群。

【19】介绍一下主从复制模式方案
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据后,需要把数据同步到从节点。

【20】说一下主从同步数据的流程
主从复制分为两个阶段:全量同步、增量同步
1.全量同步是指从节点第一次与主节点建立连接的时候使用全量同步:(1)从节点请求主节点同步数据,从节点会携带自己的replication id和offset偏移量。(2)主节点会判断是否是第一次请求,主要判断依据是两者的replication id是否相同,若相同则说明是第一次同步,那么主节点就会将自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。(3)同时主节点会执行bgsave指令,后台生成rdb快照文件,发送给从节点执行,从节点会先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保证主从节点数据一致了。
意外情况:当主节点生成rdb文件时,依然有请求发送到了主节点,那么主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后会把这个日志文件发送给从节点,这样就能保证主从节点的完全一致,后期再同步数据的时候都是依赖于这个日志文件。
2.增量同步指的是当从节点服务重启之后,数据就不一致了,所以这时从节点会请求主节点同步数据,主节点还是判断是不是第一次请求,不是的话就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据发送给从节点进行数据同步。

【21】如何保证redis的高并发高可用?
1.首先可以搭建主从集群,再加上使用redis中的哨兵模式;哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;
2.如果master发生故障,那么sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时sentinel会充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis客户端,所以一般项目都会采用哨兵模式来保证redis的高并发高可用。

【22】redis集群脑裂该怎么解决?
集群脑裂:有的时候由于网络等原因可能会出现集群脑裂的情况,就是说,由于master、slave、sentinel节点处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个slave为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在和old master那里写入数据,新master无法同步数据,当网络恢复后,sentinel会将old master降为slave,这时再从新master同步数据,这会导致old master中同步的大量数据丢失。
[解决方案]:redis的配置中可以配置:(1)设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据;(2)可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,这样可以避免大量的数据丢失

【23】redis分片集群有什么作用
作用:解决海量数据存储的问题;
1.分片集群中有多个master,每个master又配置多个slave,这样就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状况,就类似于哨兵模式了。客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

【24】redis分片集群中的数据是如何存储和读取的?
redis集群引入了哈希槽的概念,有16384个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。

【25】redis是单线程的,但为什么那么快?
1.完全基于内存,C语言编写
2.采用单线程,避免了不必要的上下文切换和可竞争条件
3.使用多路I/O复用模型,非阻塞IO

【26】能解释一下I/O多路复用模型吗?
1.I/O多路复用指利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
2.目前的I/O多路复用模型都是采用epoll模式实现的,在通知用户进程Socket就绪准备的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
消息队列篇(Rabbit&Kafka)
【1】RabbitMQ如何保证消息不丢失?
1.开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据。
2.开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列和消息都要做持久化。
3.开启消费者确认机制为auto,由Spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理。

【2】RabbitMQ消息的重复消费问题如何解决的?
可以设置一个业务唯一标识,当重复处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了。

【3】还知道别的解决方案吗?
其实这就是一个典型的幂等问题,可以采用redis分布式锁、数据库的锁都是能解决的。

【4】RabbitMQ中的死信交换机了解吗?[RabbitMQ延迟队列有了解吗]
1.我们当时校园掌上资讯项目有一个业务需要用到延迟队列,其中就是使用RabbitMQ来实现的。
延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。
2.如果消息超时未消费就会变成死信,在RabbitMQ中如果有消息成为死信,队列可以绑定一个死信交换机,在死信交换机上可以绑定其他队列,在我们发消息的时候可以按照需求指定TTL的时间,这样就实现了延迟队列的功能了。

【5】如果有100万消息堆积在MQ,如何解决?
1.提高消费者的消费能力,可以使用多线程消费任务;
2.增加更多的消费者,提高消费速度;使用工作队列模式,设置多个消费者消费同一个队列中的消息;
3.扩大队列容积,提高堆积上限
4.可以使用RabbitMQ的惰性队列,惰性队列的好处就是(1)接收到消息后直接存入磁盘而非内存 (2)消费者要消费消息时才会从磁盘中读取并加载到内存;(3)支持数百万条的消息存储;

【6】RabbitMQ的高可用机制有了解过吗?
1.可以搭建集群,集群有普通集群、镜像集群、仲裁队列三种;
2.搭建镜像集群,镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机,则镜像节点会代替成为主节点,但若是在主从同步完成前主节点宕机,则可能出现数据丢失。
【7】若出现丢失数据怎么解决呢?
可以采用仲裁队列,与镜像队列一样,都是主从模式,主从同步基于Raft协议,是强一致的。并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可。

【8】Kafka是如何保证消息不丢失的?
这个保证机制有很多,在发送消息到消费者接收消息的过程中,在每个阶段都有可能会丢失消息,所以解决的话也是从多个方面考虑:
1.生产者发送消息时,可以使用异步回调函数,如果消息发送失败,可以通过回调获取失败后的消息信息,可以考虑重试或记录日志,后边再做补偿都是可以的。同时在生产者这边还可以设置消息重试,有的时候是由于网络抖动的原因导致发送不成功,就可以使用重试机制来解决。
2.在broker中消息可能会丢失,我们可以通过kafka的复制机制来确保消息不丢失,在生产者发送消息的时候,可以设置一个acks,就是确认机制。可以设置参数为all,这样的话,当生产者发送消息到了分区之后,不仅仅只在leader分区保存确认,在follwer分区也会保存确认,只有当所有的副本都保存确认以后才算是成功发送了消息,所以,这样设置就很大程度保证了消息不会在broker丢失。
第三个有可能是在消费者端丢失消息,kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经已经消费的偏移量,默认是每隔5秒提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提交偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了。

【9】Kafka中消息的重复消费问题如何解决的?
kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5秒提交一次,可能会重复消费或丢失数据。一般都会禁用掉自动提交偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了;
为了消息的幂等,我们也可以设置唯一主键来进行区分,或者是加锁,数据库的锁,或者是redis分布式锁,都能解决幂等问题。

【10】Kafka是如何保证消费的顺序性的?
可以把消息都存储在同一个分区下,有两种方式可以进行设置:
1.发送消息时指定分区号。
2.发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值一样的话,分区肯定也是一样的。

【11】Kafka的高可用机制了解过吗?
主要有两个层面:集群和复制机制
1.集群:指的是由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务;
2.复制机制:一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中;所有的分区副本的内容都是相同的,如果leader发生故障,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性;

【12】解释一下复制机制中的ISR
ISR的意思是in-sync replica,就是需要同步复制保存的follower,其中分区副本有很多的follower,分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉之后,会优先从ISR副本列表选取一个作为leader,因为ISR是同步保存数据,数据更加的完整一些,所以优先选择ISR副本列表。

【13】Kafka数据清理机制了解过吗?
Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment,每隔分段都在磁盘上以索引(xxxx.index)和日志文件(xxxx.log)的形式存储,这样分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理。
[kafka提供了两个日志的清理策略]:
1.根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时(7天);
2.根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。这个默认关闭;
[这两个策略都可以通过kafka的broker中的配置文件进行设置]

【14】Kafka中实现高性能的设计了解过吗?
Kafka高性能是多方面协同的结果,包括宏观架构、分布式存储、ISR数据同步、以及高效的利用磁盘、操作系统特性等。主要体现在:
1.消息分区:不受单台服务器的限制,可以不受限的处理更多的数据;
2.顺序读写:磁盘顺序读写,提升读写效率;
3.页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问;
4.零拷贝:减少上下文切换及数据拷贝;
5.消息压缩:减少磁盘IO和网络IO;
6.分批发送:将消息打包批量发送,减少网络开销

Spring&SpringBoot篇
【1】什么是IOC?
IoC(Inversion of Control)即控制反转。它是一种思想不是一个技术实现。描述的是Java开发领域对象的创建以及管理的问题。
例如:现在类A依赖于类B
** 传统的开发方式:往往是在类A中手动通过new关键字来new一个B的对象出来;
** 使用IoC思想的开发方式:不通过new关键字来创建对象,而是通过IoC容器来帮助我们实例化对象。我们需要哪个对象,直接从IoC容器里面去取即可。
为什么叫控制反转?
控制:指的是对象创建(实例化、管理)的权利
反转:控制权交给外部环境(IoC容器)

【2】IoC解决了什么问题?
IoC的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。
1.对象之间的耦合度或者说是依赖程度降低;
2.资源变得容易管理;

【3】什么是Spring Bean?
1.Bean代指那些被IoC容器所管理的对象。
2.我们需要告诉IoC容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是XML文件、注解或Java配置类。

【4】将一个类声明为Bean的注解有哪些?
1.@Component:通用的注解,可标注任意类为Spring组件。如果一个Bean不知道属于哪个层,可以使用@Component注解标注;
2.@Repository:对应持久层即Dao层,主要用于数据库相关操作。
3.@Service:对应服务层,主要涉及一些复杂的逻辑,需要用到Dao层。
4.@Controller:对应Spring MVC控制层,主要用于接受用户请求并调用Service层返回数据给前端页面。

【5】Spring框架中的单例bean是线程安全的吗?
1.不是线程安全的;
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
2.Spring框架没有对单例Bean进行任何多线程的封装处理。关于单例Bean的线程安全和并发问题需要开发者自行去搞定。
3.若有Bean是多种状态的话,就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。

【6】什么是AOP?
AOP是面向切面编程,在Spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,一般可以做为公共日志保存,事务处理等。

【7】你们项目中有没有用到AOP?
1.在后台管理系统中,使用AOP来记录系统的操作日志。
2.主要思路是使用AOP中的环绕通知+切点表达式,这个表达式就是找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、方法信息、注解、请求方式等,获取到这些参数以后,保存到数据库中。

【8】Spring中的事务是如何实现的?
Spring实现的事务本质就是AOP完成的,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

【9】Spring中事务失效的场景有哪些?
1.异常捕获处理:自己处理了异常没有抛出去,就会导致事务失效,所以一般处理了异常以后,要往外抛出去。
2.抛出检查异常:如果报错也会导致事务失效,解决方案是在Spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样不管是什么异常,都会回滚事务。
3.如果方法上不是public修饰,也会导致事务失效,因为Spring为方法创建代理、添加事务通知、前提条件都是该方法是public的。

【10】Spring的Bean的生命周期
1.通过BeanDefinition来获取Bean的定义信息,比如类的全路径、是否是延迟加载、是否是单例等信息;
2.调用构造方法实例化Bean;
3.Bean的依赖注入,比如一些Set方法注入,像平时开发用的@Autowire都是这一步完成的;
4.处理Aware接口,如果某一个Bean实现了Aware接口就会重写方法执行;
5.Bean的后置处理器BeanPostProcessor#before
6.初始化方法,比如实现了InitializingBean或者自定义了方法init-method标签或@PostContruct;
7.执行Bean的后置处理器BeanPostProcessor,主要是对Bean进行增强,有可能在这里产生代理对象;
8.销毁Bean。

【11】说一下Spring中的循环引用
循环引用其实就是循环依赖,也就是两个或两个以上的Bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A;
循环依赖在Spring中是允许存在的,Spring框架依据三级缓存可以解决大部分循环依赖问题:
1.一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的Bean对象
2.二级缓存:缓存早期的Bean对象(生命周期还没走完)
3.三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

【12】循环引用的具体解决流程清楚吗?
1.先实例化A对象,同时会创建ObjectFactory对象存入三级缓存singletonFactories;
2.A在初始化的时候需要B对象,这个走B的创建逻辑;
3.B实例化完成,也会创建ObjectFactory对象存入三级缓存SingletonFactories;
4.B需要注入A,通过三级缓存获取ObjectFactory来生成一个A的对象同时存入二级缓存,这时有两种情况:一个是可能是A的普通对象,也可能是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键;
5.B通过从二级缓存earlySingletonObjects获得到A的对象后可以正常注入,B创建成功,存入一级缓存singletonObjects;
6.回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一级缓存singletonObjects;
7.二级缓存中的临时对象A清除。

【13】构造方法出现了循环依赖怎么解决?
由于Bean的生命周期中构造函数是第一个执行的,Spring框架并不能解决构造函数的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行Bean对象的创建。

【14】SpringMVC的执行流程知道吗?
1.用户发送出请求到前端控制器DispatcherServlet,这是一个调度中心;
2.DispatcherServlet收到请求调用HandlerMapping(处理器映射器);
3.HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet;
4.DispatcherServlet调用HandlerAdapter(处理器适配器);
5.HandlerAdapter经过适配调用具体的处理器(Handler/Controller);
6.Controller执行完成返回ModelAndView对象;
7.HandlerAdapter将Controller执行结果;ModelAndView返回给DispatcherServlet;
8.DispatcherServlet将ModelAndView传给ViewReslover(视图解析器);
9.ViewReslover解析后返回具体View(视图);
10.DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中);
11.DispatcherServlet响应用户。
当然现在的开发基本都是前后端分离开发的,并没有视图这些,一般都是handler中使用Response直接结果返回。

【15】SpringBoot自动装配原理是什么?
在SpringBoot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
1.@SpringBootConfiguration[注明当前类是一个配置类]
2.@EnableAutoConfiguration
3.@ComponentScan
其中@EnableAutoConfiguration是实现自动化配置的核心注解。
该注解通过@Import注解导入对应的配置选择器。关键是内部就读取了该项目和该项目引用的Jar包的classpath路径下META-INF/spring.factories文件中所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中。一般条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

【16】Spring的常见注解有哪些?
1.生命Bean:@Component、@Service、@Repository、@Controller;
2.依赖注入相关:@Autowired、@Qualifier、@Resourse
3.设置作用域:@Scope
4.spring配置相关:@Configuration、@ComponentScan、@Bean;
5.跟AOP相关做增强的注解:@Aspect、@Before、@After、@Around、@Pointcut

【17】SpringMVC常用注解有哪些?
1.@RequestMapping:用于映射请求路径;
2.@RequestBody:该注解实现接收http请求的json数据,将json转换为java对象;
3.@RequestParam:指定请求参数的名称;
4.@PathViriable:从请求路径下获取请求参数(/user/{id}),传递给方法的形式参数;
5.@ResponseBody:该注解实现将Controller方法返回对象转化为json对象相应给客户端;
6.@RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些;

【18】SpringBoot常见注解有哪些?
SpringBoot的核心注解是@SpringBootApplication,由几个注解组成:
1.@SpringBootConfiguration:组合了@Configuration注解,实现配置文件的功能;
2.@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项;
3.@ComponentScan:Spring组件扫描。

【19】MyBatis执行流程?
1.读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件;
2.构造会话工厂SqlSessionFactory,一个项目只需要一个,是单例的,一般由spring进行管理;
3.会话工厂创建SqlSession对象,这里面就包含了执行SQL语句的所有方法;
4.操作数据库的接口,Executor执行器,同时负责查询缓存的类型;
5.Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息;
6.输入参数映射;
7.输出结果映射;

【20】MyBatis是否支持延迟加载?
支持;延迟加载的意思是:在需要用到数据时才进行加载,不需要用到数据时就不加载;
MyBatis支持一对一关联对象和一对多关联集合对象的延迟加载;在MyBatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false,默认是关闭的。

【21】延迟加载的底层原理了解吗?
延迟加载在底层主要使用的CGLIB动态代理完成的;
1.使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper;
2.当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询;
3.获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了;
【22】MyBatis的一级、二级缓存用过吗?
1.mybatis的一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存;
2.二级缓存需要单独开启;二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQL session,默认也是采用PerpetualCache,HashMap存储。如果想开启二级缓存需要在全局配置文件和映射文件中开启配置才行。

【23】MyBatis的二级缓存什么时候会清理缓存中的数据?
当某一个作用域(一级缓存Session/二级缓存Namespaces)进行了新增、修改、删除操作后,默认该作用域下所有select中的缓存将被clear。

微服务篇
【1】SpringCloud的5大组件有哪些?
早期:

  • Eureka:注册中心
  • Ribbon:负载均衡
  • Feign:远程调用
  • Hystrix:服务熔断
  • Zuul/Gateway:网关
    随着SpringCloudAlibaba在国内兴起,现期5大组件如:
  • Nacod:注册中心/配置中心
  • Ribbon:负载均衡
  • Feign:服务调用
  • Sentinel:服务保护
  • Gateway:服务网关

【2】服务注册和发现是什么意思?SpringCloud如何实现服务注册发现?
我理解的是主要有三块大功能:分别是服务注册、服务发现、服务状态监控;比如Eureka:
1.服务注册:服务提供者需要把自己的信息注册到Eureka,由Eureka来保存这些信息,比如服务名称、ip、端口等等;
2.服务发现:消费者想Eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用;
3.服务监控:服务提供者会每隔30秒向Eureka发送心跳,报告健康状态,如果Eureka服务90秒没接收到心跳,就会将该服务从Eureka中剔除。

【3】Nacos和Eureka有什么区别?
Nacos支持配置中心;
共同点:Nacos与Eureka都支持服务注册和服务拉取,都支持服务提供者心跳方式做健康监测;
区别:
1.Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式;
2.临时实例心跳不正常会被剔除,非临时实例则不会被剔除;
3.Nacos支持服务列表变更的消息推送模式,服务列表更新更及时;
4.Nacos集群默认采用高可用(AP)方式,当集群中存在非临时实例时,采用强一致(CP)模式;Eureka采用AP方式;

【4】负载均衡是如何实现的?
1.在服务调用过程中的负载均衡一般使用SpringCloud的Ribbon组件实现,Feign的底层已经自动集成了Ribbon,使用起来非常简单;
2.当发起远程调用时,Ribbon先从注册中心拉取服务地址列表,然后按照一定的路由策略选择一个发起远程调用,一般的调用策略是轮询。

【5】Ribbon负载均衡策略有哪些?
有很多种:

  • RoundRobinRule:简单轮询服务列表来选择服务器;
  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小;
  • RandomRule:随机选择一个可用的服务器;
  • ZoneAvoidanceRule:区域敏感策略,以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等;而后再对Zone内的多个服务做轮询(默认)。

【6】如果想自定义负载均衡策略如何实现?
1.创建类实现IRule接口,可以指定负载均衡策略,这个是全局的,对所有的远程调用都起作用;
2.在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略,只是对配置的这个服务生效远程调用。

【7】什么是服务雪崩,怎么解决这个问题?
服务雪崩是指一个服务失败,导致整条链路的服务都失败的情形,一般我们在项目解决的话就是两种方案,第一个是服务降级;第二个是服务熔断;如果流量太大的话,可以考虑限流;
1.服务降级:是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与Feign接口整合,编写降级逻辑;
2.服务熔断:默认关闭,需要手动打开[引导类上加上@EnableCircuitBreaker],如果检测到10秒内请求的失败率超过50%,就触发熔断机制。之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。

【8】微服务是怎么监控的?
1.采用SkyWalking进行监控,其能够监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,然后可以针对性的分析和优化。
2.可以在skywalking设置告警规则,特别是项目上线以后,如果报错,分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复。

【9】有没有做过限流?怎么做的?
1.采用nginx限流操作,nginx使用的漏桶算法来实现过滤, 让请求以固定的速率处理请求,可以应对突发流量,我们控制的速率是按照ip进行限流,限制的流量是每秒20;
2.采用spring cloud gateway中的支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法,可以根据ip或路径进行限流,可以设置每秒填充平均速率和令牌桶总容量。

【10】限流常见的算法有哪些?
比较常见的限流算法有漏桶算法和令牌桶算法;
1.漏桶算法是把请求存入桶中,以固定速率从桶中流出,可以让我们的服务做到绝对的公平,起到了很好的限流效果;
2.令牌桶算法在桶中存储的是令牌,按照一定的速率生成令牌,每个请求都要先申请令牌,申请到令牌以后才能正常请求,也可以起到很好的限流作用

[区别]:漏桶和令牌桶都可以处理突发流量,其中漏桶可以做到绝对的平滑,令牌桶有可能会产生突发大量请求的情况,一般nginx限流采用的漏桶,spring cloud gateway中可以支持令牌桶算法。

【11】什么是CAP理论?
CAP主要是在分布式项目下的一个理论。包括了一致性、可用性、分区容错性。
1.一致性(Consistentcy):指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。
2.可用性(Availability):指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果;
3.分区容错性(Partition tolerance):指分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

【12】为什么分布式系统中无法同时保证一致性和可用性?
1.如果保证了一致性©:对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到失败或超时。这与可用性是相悖的;
2.如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。

【13】什么是BASE理论?
BASE是CAP理论中AP方案的延伸,核心思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。思想如下:
1.Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用;
2.Soft state(软状态):即指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
3.Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

【14】可以采用哪种分布式事务解决方案?
可以使用seata的at模式解决分布式事务:
1.阶段一RM的工作:(1)注册分支事务(2)记录undo-log数据快照(3)执行业务sql并提交(4)报告事务状态
2.阶段二提交时RM的工作:删除undo-log即可;
3.阶段二回滚时RM的工作:根据undo-log恢复数据到更新前
[at模式牺牲了一致性,保证了可用性,不过它保证了最终一致性]

【15】分布式服务的接口幂等性如何设计?
幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致;
可以采用token+redis实现:
1.第一次请求下单时在后台为用户生成一个唯一token存入redis,key就是用户的id,value就是这个token,同时把这个token返回给前端;
2.第二次请求,当用户点击下单操作后会携带之前的token,后台先到redis进行验证,若存在则可以执行业务,同时删除token;如果不存在,则直接返回,不处理业务,就保证了同一个token只处理一次业务,就保证了幂等性。

【16】xxl-job路由策略有哪些?
轮询、故障转移、分片广播

【17】xxl-job任务执行失败怎么解决?
1.路由策略选择故障转移,优先使用健康的实例来执行任务;
2.如果还有失败的,在创建任务时,可以设置重试次数;
3.如果还有失败的,就可以查看日志或者配置邮件告警来通知相关负责人来解决

【18】如果有大数据量的任务同时都需要执行,怎么解决?
部署多个实例共同去执行这些批量任务,其中任务的路由策略时分片广播;在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行就可以了。

计算机网络篇
【1】从输入URL到页面展示到底发生了什么?
1.浏览器接收到用户请求,先检查浏览器缓存里是否有缓存该资源,如果有直接返回;如果没有进入下一步网络请求。
2.网络请求前,进行DNS解析,以获取请求域名的IP地址。如果请求协议是HTTPS的话,那么还需要建立TLS连接。DNS解析时会按本地浏览器缓存->本地Host文件->路由器缓存->DNS服务器->根DNS服务器的顺序查询域名对应IP,直到找到为止。
3.浏览器与服务器IP建立TCP连接。连接建立后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的Cookie等数据附加到请求头中,向服务器构建请求信息。
4.服务器接收到请求信息后,根据请求生成响应数据。
5.浏览器解析响应头。若响应头状态码为301、302,会重定向到新地址;若响应数据类型是字节流类型,一般会将请求提交给下载管理器;若是HTML类型,会进入下一步渲染流程。
6.浏览器解析HTML文件,创建DOM树,解析CSS进行样式计算,然后将CSS和DOM合并,构建渲染树;最后布局和绘制渲染树,完成页面展示。

【2】三次握⼿的过程,以及为什么是三次,⽽不是四次,两次?
三次握手的过程如下:
1.客户端向服务器发送SYN报文、初始化序列号ISN(seq=x),然后客户端进入SYN_SEND状态,等待服务器确认。
2.服务端发送ACK确认客户端的SYN报文(ack=x+1),同时发出一个SYN报文,带上自己的初始化序列号(seq=y),然后服务端进入SYN_RECV状态。
3.客户端接收到服务端的SYN、ACK报文,ACK确认服务端的SYN报文(ACK=y+1),然后客户端和服务端都进入ESTABLISHED状态,完成TCP三次握手。

为什么不是四次握⼿? 为什么不能两次握⼿?
因为三次握⼿才能保证双⽅具有接收和发送的能⼒。 两次握⼿可能导致资源的浪费,由于没有第三次握⼿,服务端就⽆法确认客户端是否收到了⾃⼰的回复,所以每收到⼀个 SYN ,服务器都会主动去建⽴⼀个连接, ⽽四次握⼿可以优化为三次。

【3】四次挥⼿的过程,以及为什么是四次?
四次挥⼿的过程:

  1. 客户端发送⼀个FIN报⽂给服务端,表示⾃⼰要断开数据传送,报⽂中会指定⼀个序列号(seq=x)。然后,客户端进⼊FIN-WAIT-1状态。
  2. 服务端收到FIN报⽂后,回复ACK报⽂给客户端,且把客户端的序列号值+1,作为ACK+1报⽂的序列号(seq=x+1) 。然后,服务端进⼊CLOSE-WAIT(seq=x+1)状态,客户端进⼊FIN-WAIT-2状态。
  3. 服务端也要断开连接时,发送FIN报⽂给客户端,且指定⼀个序列号(seq=y+1) ,随后服务端进⼊LAST-ACK状态。
  4. 客户端收到FIN报⽂后,发出ACK报⽂进⾏应答,并把服务端的序列号值+1作为 ACK 报⽂序列号(seq=y+2)。此时客户端进⼊ TIME-WAIT 状态。服务端在收到客户端的 ACK报⽂后进⼊ CLOSE 状态。如果客户端等待 2MSL 没有收到回复,才关闭连接

为什么是四次挥手?
TCP 是全双⼯通信,可以双向传输数据。任何⼀⽅都可以在数据传送结束后发出连接释放的通知,待对⽅确认后进⼊半关闭状态。 当另⼀⽅也没有数据再发送的时候,则发出连接释放通知,对⽅确认后才会完全关闭了 TCP 连接。 总结:两次握⼿可以释放⼀端到另⼀端的 TCP 连接,完全释放连接⼀共需要四次握⼿。

【4】TCP与UDP的概念,特点,区别和对应的使⽤场景?

  1. TCP与UDP的概念
    TCP(传输控制协议)是⼀种⾯向连接的、可靠的、基于字节流的传输层通信协议。
    UDP(⽤户数据报协议)为应⽤程序提供了⼀种⽆需建⽴连接就可以发送封装的IP数据包的⽅法。
  2. 特点
    TCP :⾯向连接,传输可靠,传输形式为字节流,传输效率慢,所需资源多。
    UDP :⽆连接、传输不可靠,传输形式为数据报⽂段,传输效率快,所需资源少。
  3. 区别
    是否⾯向连接: TCP 是⾯向连接的传输,UDP 是⽆连接的传输。
    是否是可靠传输:TCP是可靠的传输服务,在传递数据之前,会有三次握⼿来建⽴连接;在数据传递时,有确认、窗⼝、重传、拥塞控制机制。 UDP是不可靠传输,数据传递不需要给出任何确认,且不保证数据不丢失及到达顺序。
    是否有状态:TCP 传输是有状态的,它会去记录⾃⼰发送消息的状态⽐如消息是否发送了、是否被接收了等等,⽽ UDP 是⽆状态的。
    传输形式: TCP 是⾯向字节流的,UDP 是⾯向报⽂的。
    传输效率:由于TCP 传输的时候多了连接、确认重传等机制,所以TCP 的传输效率要⽐UDP 低。
    ⾸部开销 :TCP ⾸部开销 (20 ~ 60字节)⽐UDP ⾸部开销 (8字节)要⼤。
    是否提供⼴播或多播服务: TCP 只⽀持点对点通信;UDP ⽀持⼀对⼀、⼀对多、多对⼀、多对多。
  4. 对应的使⽤场景
    TCP常⽤于要求通信数据可靠场景(如⽹⻚浏览、⽂件传输、邮件传输、远程登录、数据库操作等)。
    UDP常⽤于要求通信速度⾼场景(如域名转换、视频直播、实时游戏等)。

【5】HTTP请求常⻅的状态码和字段
HTTP常见字段:
1.Hosts字段:客户端发送请求时,用来指定服务器的域名
2.Content-length字段:服务器返回数据时,带有该字段,表示返回的数据长度
3.Connection字段:常用于客户端要求服务器使用HTTP长连接时使用
4.Conent-Type字段:服务器回应时,告诉客户端本次数据的格式
5.Content-Encoding字段:服务器返回的数据使用了什么压缩格式

HTTP状态码:
1.1XX:提示信息,协议处理的中间状态
2.2XX:请求成功
3.3XX:请求重定向
4.4XX:请求错误
5.5XX:服务器错误
【6】常⻅的请求⽅式?GET和POST请求的区别?

  1. 作⽤不同
    GET⽤于从服务端获取资源
    POST⼀般⽤来向服务器端提交数据
  2. 参数传递⽅式不同
    GET请求的参数⼀般写在URL中,且只接受ASCII字符
    POST请求参数⼀般放在请求体中,对于数据类型也没有限制
  3. 安全性不同
    因为参数传递⽅式的不同,所以两者安全性不同,GET请求的参数直接暴露在URL中,所以更不安全,不能⽤来传递敏感信息。
  4. 参数⻓度限制不同
    GET传送的数据量较⼩,不能⼤于2KB。
    POST传送的数据量较⼤,⼀般被默认为不受限制。
    HTTP 协议没有 Body 和 URL 的⻓度限制,对 URL 限制的⼤多是浏览器和服务器的原因。
  5. 编码⽅式不同
    GET 请求只能进⾏ URL 编码(application/x-www-form-urlencoded)
    POST ⽀持多种编码⽅式(application/x-www-form-urlencoded 或 multipart/form-data。为⼆
    进制数据使⽤多种编码。)
  6. 缓存机制不同
    GET 请求会被浏览器主动cache,⽽ POST 不会,除⾮⼿动设置。GET 请求参数会被完整保留在浏览器历史记录⾥,⽽ POST 中的参数不会被保留。GET 产⽣的 URL 地址可以被 保存为书签,⽽ POST 不可以。GET 在浏览器回退时是⽆害的,⽽ POST 会再次提交请求。
  7. 时间消耗不同
    GET 产⽣⼀个 TCP 数据包;
    POST 产⽣两个 TCP 数据包。
    对于 GET ⽅式的请求,浏览器会把 header 和 data ⼀并发送出去,服务器响应 200(返回数据);
    ⽽对于 POST,浏览器先发送 Header,服务器响应 100 continue,浏览器再发送 data,服务器响
    应 200 ok(返回数据)
  8. 幂等
    意思是多次执⾏相同的操作,结果都是「相同」的。GET ⽅法就是安全且幂等的,因为它是「只读」操作,⽆论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。

【7】HTTPS的⼯作原理?(https是怎么建⽴连接的)

  1. ⾸先,客户端向服务器端发送请求报⽂,请求与服务端建⽴连接。
  2. 服务端产⽣⼀对公私钥,然后将⾃⼰的公钥发送给CA机构,CA机构也有⼀对公私钥,然后CA机构使⽤⾃⼰的私钥将服务端发送过来的公钥进⾏加密,产⽣⼀个CA数字证书。
  3. 服务端响应客户端的请求,将CA机构⽣成的数字证书发送给客户端。
  4. 客户端将服务端发送过来的数字证书进⾏解析(因为浏览器产商跟CA机构有合作,所以浏览器中已经保存了⼤部分CA机构的密钥,⽤于对服务端发送过来的数字证书进⾏解密),验证这个数字证书是否合法,如果不合法,会发送⼀个警告。如果合法,取出服务端⽣成的公钥。
  5. 客户端取出公钥并⽣成⼀个随机码key(其实就是对称加密中的密钥)
  6. 客户端将加密后的随机码key发送给服务端,作为接下来的对称加密的密钥
  7. 服务端接收到随机码key后,使⽤⾃⼰的私钥对它进⾏解密,然后获得到随机码key。
  8. 服务端使⽤随机码key对传输的数据进⾏加密,将传输加密后的内容给客户端
  9. 客户端使⽤⾃⼰⽣成的随机码key解密服务端发送过来的数据,之后,客户端和服务端通过对称加密传输数据,随机码Key作为传输的密钥。

【8】HTTPS与HTTP的区别
1.HTTP 是明⽂传输,⽽HTTPS 通过 SSL\TLS 进⾏了加密
2.HTTP 的端⼝号是 80,HTTPS 是 443
3.HTTPS 需要到 CA 申请证书
4.HTTP 的连接简单,是⽆状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进⾏加密传输、身份认证的⽹络协议,⽐ HTTP 协议安全。

【9】DNS是什么,及其查询过程
DNS(Domain Name System)域名管理系统,是当⽤户使⽤浏览器访问⽹址之后,使⽤的第⼀个重
要协议。DNS 要解决的是域名和 IP 地址的映射问题。

  1. ⾸先⽤户在浏览器输⼊URL地址后,会先查询浏览器缓存是否有该域名对应的IP地址。
  2. 如果浏览器缓存中没有,会去计算机本地的Host⽂件中查询是否有对应的缓存。
  3. 如果Host⽂件中也没有则会向本地的DNS解析器(通常由你的互联⽹服务提供商(ISP)提供)
    发送⼀个DNS查询请求。
  4. 如果本地DNS解析器没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服
    务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的
    DNS服务器继续查询。
  5. 本地DNS解析器接着向指定的顶级域DNS服务器发出查询请求。顶级域DNS服务器也不负责具体
    的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下⼀步的信息。
  6. 本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP
    地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找"example.com"域名对应的IP
    地址,并将结果返回给本地DNS解析器。
    7.本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应。
    【10】HTTP多个TCP连接怎么实现
    多个TCP连接是靠某些服务器对 Connection: keep-alive 的 Header 进⾏了⽀持。简⽽⾔
    之,完成这个 HTTP 请求之后,不要断开 HTTP 请求使⽤的 TCP 连接。这样的好处是连接可以被重
    新使⽤,之后发送 HTTP 请求的时候不需要重新建⽴ TCP 连接,以及如果维持连接,那么 SSL 的
    开销也可以避免。

【11】TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是⼀个东⻄吗?
HTTP 的 Keep-Alive,是由应⽤层(⽤户态)实现的,称为 HTTP ⻓连接;
每次请求都要经历这样的过程:建⽴ TCP -> 请求资源 -> 响应资源 -> 释放连接,这就是HTTP短连接,但是这样每次建⽴连接都只能请求⼀次资源,所以HTTP 的 Keep-Alive实现了使⽤同⼀个TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建⽴和释放的开销,这就是 HTTP ⻓连接。
TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制;
通俗地说,就是TCP有⼀个定时任务做倒计时,超时后会触发任务,内容是发送⼀个探测报⽂给对端,⽤来判断对端是否存活。

【12】TCP连接如何确保可靠性
1.数据块⼤⼩控制:应⽤数据被分割成TCP认为最合适发送的数据块,再传输给⽹络层,数据块被称为报⽂段或段。
2.序列号: TCP给每个数据包指定序列号,接收⽅根据序列号对数据包进⾏排序,并根据序列号对数据包去重。
3.校验和: TCP将保持它⾸部和数据的校验和。这是⼀个端到端的检验和,⽬的是检测数据在传输过程中的任何变化。如果收到报⽂的检验和有差错,TCP将丢弃这个报⽂段和不确认收到此报⽂段。
4.流量控制: TCP连接的每⼀⽅都有固定⼤⼩的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收⽅来不及处理发送⽅的数据,能提示发送⽅降低发送的速率,防⽌包丢失。TCP利⽤滑动窗⼝实现流量控制。
5.拥塞控制: 当⽹络拥塞时,减少数据的发送。
6.确认应答: 通过 ARQ 协议实现。基本原理是每发完⼀个分组就停⽌发送,等待对⽅确认。如果没收到确认,会重发数据包,直到确认后再发下⼀个分组。
7.超时重传: 当TCP发出⼀个数据段后,它启动⼀个定时器,等待⽬的端确认收到这个报⽂
段。如果不能及时收到⼀个确认,将重发这个报⽂段。

【13】Cookie和Session是什么?有什么区别?
Cookie 和 Session 都⽤于管理⽤户的状态和身份, Cookie 通过在客户端记录信息确定⽤户身份, Session 通过在服务器端记录信息确定⽤户身份。

  1. Cookie
    Cookie 是存储在⽤户浏览器中的⼩型⽂本⽂件,⽤于在⽤户和服务器之间传递数据。通常,服务器会将⼀个或多个 Cookie 发送到⽤户浏览器,然后浏览器将这些 Cookie 存储在本地。服务器在接收到来⾃客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从⽽动态⽣成与该客户端相对应的内容。
  2. Session
    客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session 。Session 主要⽤于维护⽤户登录状态、存储⽤户的临时数据和上下⽂信息等。
    存储位置:Cookie 数据存储在⽤户的浏览器中,⽽ Session 数据存储在服务器上。
    数据容量:Cookie 存储容量较⼩,⼀般为⼏ KB。Session 存储容量较⼤,通常没有固定限制,取决于服务器的配置和资源。
    安全性:由于 Cookie 存储在⽤户浏览器中,因此可以被⽤户读取和篡改。相⽐之下,Session 数据存储在服务器上,更难被⽤户访问和修改。
    传输⽅式:Cookie 在每次 HTTP 请求中都会被⾃动发送到服务器,⽽ Session ID 通常通过 Cookie 或 URL 参数传递。
  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值