一、Java基础

1.1、Java语言基础

1、JVM vs JRE vs JDK⭐⭐⭐⭐
  • JVM是Java程序运行的虚拟机,负责将字节码转换成机器码并执行。
  • JRE是Java程序的运行环境,包含了JVM和必要的类库,用于运行已经编译好的Java程序。
  • JDK是Java开发工具包,包含了JRE和开发工具,用于开发Java程序。

这三者之间的关系是层层嵌套的:JDK > JRE > JVM。在开发Java程序时,需要安装JDK;而在运行Java程序时,只需要安装JRE即可。

2、Java 中的几种基本数据类型⭐⭐⭐⭐⭐
  • 4 种整数型:byte(1字节)、short(2字节)、int(4字节)、long(8字节)
  • 2 种浮点型:float(4字节)、double(8字节)
  • 1 种字符类型:char(2字节)
  • 1 种布尔型:boolean(特殊,1字节,但具体实现可能依赖于虚拟机)
3、基本类型和包装类型的区别? ⭐⭐⭐
  • 用途:包装类型可用于泛型,而基本类型不可以。
  • 存储方式:基本数据类型的局部变量存放在栈中,基本数据类型的成员变量存放在Java 虚拟机的堆中。包装类型属于对象类型存在于堆中。
  • 占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。
  • 默认值:包装类型不赋值就是 null ,而基本类型有默认值且不是 null。
  • 比较方式:对于基本数据类型来说,= =比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。
4、包装类型的缓存机制 ⭐⭐
  • Byte Short Integer Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据
  • Character 创建了数值在 [0,127] 范围的缓存数据
  • Boolean 直接返回 True or False
  • 两种浮点数类型的包装类 Float,Double 并没有实现缓存机制

如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小是在性能和资源之间的权衡。

5、自动装箱与拆箱是什么及其原理? ⭐⭐
  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。因此,Integer i = 10 (自动装箱)等价于 Integer i = Integer.valueOf(10)。int n = i(自动拆箱) 等价于 int n = i.intValue();

6、为什么浮点数运算会有精度丢失的风险?如何解决? ⭐⭐⭐

为什么:计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。
解决:BigDecimal 可以实现对浮点数的运算,不会造成精度丢失。用BigDecimal的compareTo去比较。

7、静态方法和实例方法有何不同? ⭐⭐⭐
维度 静态方法(Static Method) 实例方法(Instance Method)
修饰符 必须用 static static
调用方式 类名直接调用(推荐),也可以对象名调用(不推荐) 必须通过对象实例调用
访问成员 只能访问静态成员,不能用 this 可访问所有成员,可用 this/super
内存分配 类加载时分配,唯一副本 对象创建时关联,随对象存在
8、成员变量与局部变量的区别? ⭐⭐⭐
  • 定义位置:成员变量是在类中定义的,但在方法外部。它们是类的一部分,并且可以被类的所有方法访问。局部变量是在方法、构造函数或代码块内部定义的。它们的作用范围仅限于定义它们的代码块内
  • 生命周期:成员变量生命周期与对象的生命周期相同。它们在对象创建时被分配内存,直到对象被垃圾回收时才释放内存。局部变量生命周期仅限于定义它们的代码块执行期间。当代码块执行完毕后,局部变量的内存会被释放。
  • 默认值:成员变量有默认值。如果没有显式初始化,它们会被自动初始化为默认值。局部变量没有默认值。在使用之前必须显式初始化,否则编译器会报错。
  • 访问权限:成员变量可以使用访问修饰符(如 public、private、protected、默认)来控制访问权限。局部变量没有访问修饰符。它们的作用范围完全受限于定义它们的代码块。
9、为什么 Java 只有值传递? ⭐⭐⭐

基本数据类型(如 int、float、char 等)在方法调用时传递的是值的副本。这意味着方法内部对参数的修改不会影响方法外部的原始变量。
对于对象,传递的是对象引用的副本,而不是对象本身。这意味着虽然引用(指针)的副本被传递到方法中,但引用指向的实际对象是共享的

  • 传递的是引用的副本:在方法内对对象属性的修改会影响原始对象,因为引用指向的是同一个对象。

  • 但引用本身是值传递:即方法内对引用的重新赋值不会影响原始引用的值。

1.2、面向对象编程

1、重载和重写有什么区别? ⭐⭐⭐⭐⭐
  • 定义:重载指在同一个类中定义多个方法,它们的名称相同,但参数列表不同(参数的数量、类型或顺序不同),返回类型可以相同也可以不同。重写指子类重新定义从父类继承的方法,方法名称、参数列表、返回类型都必须与父类中的方法完全相同。
  • 目的:重载主要用于增加方法的灵活性,使得一个方法名称可以用于不同的参数类型和数量,可以使代码更具可读性和简洁性。重写主要用于在子类中提供父类方法的特定实现,允许子类根据需要修改或扩展父类的行为,从而实现多态性。
  • 访问修饰符:重载访问修饰符可以不同。重写访问修饰符不能减少父类方法的访问权限,即子类重写的方法访问修饰符必须与父类中相应的方法的访问修饰符相同或更宽松
  • 异常处理:在重载方法中,异常类型和数量可以不同。重载方法的异常处理不受限制。在重写方法中,子类方法可以抛出比父类方法更少或相同的异常,但不能抛出更多或不同的异常。这是为了保持父类和子类的一致性。
  • 发生阶段:重载方法发生在编译期,重写方法发生在运行期。
2、面向对象和面向过程的区别 ⭐⭐
  • 面向过程编程(POP):将问题分解为一系列步骤(函数或过程),代码围绕 “怎么做” 展开。数据和操作数据的函数是分离的。
  • 面向对象编程(OOP):数据(属性)和操作数据的函数(方法)封装到 “对象” 中,代码围绕 “谁来做” 展开。对象通过消息传递(方法调用)交互。

相比较于 POP,OOP 开发的程序一般具有下面这些优点:

  • 易维护:由于良好的结构和封装性,OOP 程序通常更容易维护。
  • 易复用:通过继承和多态,OOP 设计使得代码更具复用性,方便扩展功能
  • 易扩展:模块化设计使得系统扩展变得更加容易和灵活。
3、Java 创建对象有几种方式 ⭐⭐⭐
  • new关键字:这是最常见的创建对象的方式。通过调用类的构造函数,使用new关键字可以在内存中分配一个新的对象。
Person person=new Person();
  • 反射:Java的反射机制允许在运行时动态地创建对象。通过获取类的Class对象,并调用其构造函数,可以实现对象的创建。
Class clazz=Class.forName("Person");
Person person=(Person)clazz.getDeclaredConstructor().newInstance();
  • clone()方法:如果类实现了Cloneable接口,并重写 clone() 方法:就可以使用clone()方法创建对象的副本。
Person person2 = (Person) person1.clone();
  • 对象的反序列化:通过将对象序列化到一个字节流中,然后再进行反序列化,可以创建对象的副本,。
FileInputStream fileInputStream = new FileInputStream("person.ser");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
Person person = (Person) objectInputStream.readObject();

其中,使用new关键字是最常见和推荐的创建对象的方式。其他方式通常在特定场景下使用,如需要动态创建对象或创建对象的副本等情况。

4、构造方法有哪些特点?是否可被 override? ⭐⭐
  • 名称与类名相同:构造方法的名称必须与类名完全一致。
  • 没有返回值:构造方法没有返回类型,且不能使用 void 声明。
  • 自动执行:在生成类的对象时,构造方法会自动执行,无需显式调用。

构造方法不能被重写(override),但可以被重载(overload)。因此,一个类中可以有多个构造方法,这些构造方法可以具有不同的参数列表,以提供不同的对象初始化方式。

5、面向对象三大特征⭐⭐⭐⭐
  • 封装是指将对象的状态(属性)和行为(方法)结合在一起,并将这些实现细节隐藏在对象内部,只暴露出接口给外部使用。通过封装,对象的内部状态可以被保护,不被外部直接访问,从而减少系统复杂性,提高安全性和灵活性。
  • 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而实现代码重用。子类可以继承父类的公共和保护成员,并可以对其进行扩展和修改。
  • 多态是指不同的对象对同一消息做出不同的响应。多态有两种主要形式:方法重载(编译时多态)和方法重写(运行时多态)。
6、接口和抽象类有什么共同点和区别? ⭐⭐⭐⭐⭐
维度 抽象类(Abstract Class) 接口(Interface)
定义方式 使用 abstract class 声明,可包含抽象方法和非抽象方法 使用 interface 声明,方法默认是 public abstract,字段默认是 public static final
实现关键字 子类通过 extends 继承抽象类,只能单继承 类通过 implements 实现接口,可同时实现多个接口
方法实现 可以包含具体方法的实现(非抽象方法) 方法不能有实现(JDK8+ 允许默认方法和静态方法实现)
字段属性 可以有普通成员变量,也可以有静态变量 字段必须是 public static final(默认省略修饰符),不可修改
构造方法 可以有构造方法(供子类调用初始化) 没有构造方法,无法实例化
设计目的 用于抽取同类事物的公共属性和行为,体现 “is-a” 关系(继承) 用于定义行为规范,体现 “can-do” 关系(实现),强调功能拓展
7、深拷贝和浅拷贝区别了解吗?什么是引用拷贝? ⭐⭐⭐
  • 浅拷贝:浅拷贝创建一个新的对象,但并不复制对象所引用的内部对象
  • 深拷贝:深拷贝创建一个新的对象,并递归地复制对象内部所引用的所有对象。
  • 引用拷贝 引用拷贝并不真正复制对象,而是创建一个新的引用指向原来的对象。
    在这里插入图片描述
8、Object 类的常见方法有哪些? ⭐⭐⭐⭐
//native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public final native Class<?> getClass()

 //native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。
public native int hashCode()

//用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。
public boolean equals(Object obj)

//native 方法,用于创建并返回当前对象的一份拷贝。
protected native Object clone() throws CloneNotSupportedException

//返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString()

//native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notify()

//native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void notifyAll()

//native方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间
public final void wait() throws InterruptedException

//实例被垃圾回收器回收的时候触发的操作
protected void finalize() throws Throwable { }

9、equals与== 区别⭐⭐⭐⭐⭐

在Java中,==是一个比较操作符,用于比较两个变量的值是否相等。而"equals()"是Object类中定义的方法,用于比较两个对象是否相等
具体区别如下:

  • ==用于比较基本数据类型和引用类型变量的地址值是否相等。对于基本数据类型,比较的是它们的实际值;对于引用类型,比较的是它们所引用的对象的地址值。
  • equals()方法用于比较两个对象的内容是否相等。默认情况下,它与 = = == ==的作用相同,比较的是对象的地址值。但是,可以根据具体的类重写该方法,以实现自定义的比较逻辑,String已经重写了equals方法。
10、hashCode()有什么用⭐⭐⭐
  • 核心功能 返回对象的哈希码(整数),用于确定对象在哈希表(如 HashMap、HashSet、Hashtable)中的存储位置(索引)。

  • 在哈希表中的使用场景(以HashSet为例)
    步骤1:添加对象时,先计算 hashCode(),根据哈希码定位对象在哈希表中的存储位置。
    步骤2:若该位置无其他对象,直接存入(无需调用equals())。
    步骤3:若该位置已有对象,则调用 equals() 比较两者是否相等: 相等 → 不存入(避免重复)。 不相等 →通过哈希冲突解决机制(如链表/红黑树)存入其他位置。

  • 性能优势:通过 hashCode() 快速定位数据,减少 equals() 调用次数,从而提升哈希表操作效率(如查询、去重)

11、为什么重写 equals() 时必须重写 hashCode() 方法?⭐⭐⭐⭐

hashCode() 和 equals() 方法之间有一个重要的契约(约定)。这个契约规定了,如果两个对象通过 equals() 方法比较认为是相等的(即 equals() 返回 true),那么这两个对象的 hashCode() 方法必须返回相同的哈希码。这个契约是确保哈希表(如 HashMap、HashSet)等哈希数据结构正常工作的基础。

1.3、字符串处理

1、String、StringBuffer、StringBuilder 的区别? ⭐⭐⭐⭐⭐
  • String:不可变,线程安全,适合不需要修改的字符串。
  • StringBuffer:可变,线程安全,适合多线程环境中频繁修改的字符串。
  • StringBuilder:可变,不线程安全,适合单线程环境中频繁修改的字符串。
2、String 为什么是不可变的? ⭐⭐⭐

不可变原理:

  • final 类:String 类被声明为 final,这意味着它不能被继承,从而防止了通过继承改变其行为。

  • final 字段:String 类中的 value 字段是 final 的,这保证了 String 对象在创建后其内部字符数组不会被改变。

  • 没有提供修改方法:String 类没有提供任何可以修改其内容的方法。所有对 String 的操作(如拼接、替换等)都会生成一个新的String 对象。

为什么设计成不可变

  • 线程安全:在多线程环境下,不可变对象可以被安全共享,无需额外同步机制。
  • 缓存哈希值: String 的哈希值在创建时被缓存(hash 字段),避免重复计算。
  • 字符串常量池优化:JVM 维护字符串常量池(String Pool),相同字面量的字符串共享同一实例。
3、字符串拼接用“+” 还是 StringBuilder? ⭐⭐
  • “+”运算符的底层机制:在单行代码中通过“+”拼接字符串(例如 String s = a+ b + c;),编译器会自动优化为一个 StringBuilder,调用其 append() 方法完成拼接,最终通过 toString() 生成新字符串。这种方式在简单场景下高效且代码简洁。
  • 循环中使用“+”的缺陷:在循环内使用“+”拼接字符串时,编译器无法优化为单个 StringBuilder,每次循环都会创建新的 StringBuilder 对象。
  • 显式使用 StringBuilder 的优势:直接在循环中使用 StringBuilder,可以复用同一个对象,避免内存浪费
4、字符串常量池的作用 ⭐⭐⭐

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 在堆中创建字符串对象”ab“
// 将字符串对象”ab“的引用保存在字符串常量池中
String aa = "ab";
// 直接返回字符串常量池中字符串对象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true

5、String s1 = new String(“abc”);创建了几个字符串对象?⭐⭐⭐
  • 如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中
  • 如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”

1.4、异常处理

1、Exception 和 Error 有什么区别? ⭐⭐⭐⭐⭐

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类:Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,Exception 又可以分为 Checked Exception 和 Unchecked Exception 。Error:属于程序无法处理的错误 ,不建议通过catch捕获

  • Error:表示系统级不可恢复的故障。例如 栈溢出(StackOverflowError)、虚拟机内存不够错误(OutOfMemoryError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

  • Checked Exception 预期可能发生的异常(如文件不存在) ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws关键字处理的话,就没办法通过编译。除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常。
    常见的受检查异常有:IO 相关的异常、ClassNotFoundException、SQLException、IOException。

  • Unchecked Exception 编程错误(如空指针),Java 代码在编译过程中,我们即使不处理不受检查异常也可以正常通过编译。RuntimeException及其子类都统称为非受检查异常,常见的有(NullPointerException(空指针错误)、IllegalArgumentException(参数错误比如方法入参类型错误)、ArrayIndexOutOfBoundsException(数组越界错误)、ClassCastException(类型转换错误)

2、try-catch-finally 如何使用? ⭐⭐⭐⭐⭐
  • try块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
  • catch块:用于处理 try 捕获到的异常。
  • finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return语句时,finally 语句块将在方法返回之前被执行。

注意:不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。

3、finally 中的代码一定会执行吗? ⭐⭐⭐⭐

在某些情况下,finally 中的代码不会被执行:

  • finally 之前虚拟机被终止运行。System.exit(0); 强制终止 JVM
  • 线程中断:当线程在执行 try 或 finally 代码块时被中断(interrupt()),
  • JVM 崩溃:若在执行 finally 前 JVM 发生崩溃(如内存溢出且无法处理),finally 无法执行。
5、如何使用 try-with-resources 代替try-catch-finally? ⭐⭐⭐

在 Java 中,try-with-resources 语句是处理资源管理的一种简洁方法。它可以自动关闭资源,避免了显式的 finally 块和手动关闭资源的复杂性。try-with-resources 语句的引入主要是为了简化资源管理,特别是在处理如文件、流、数据库连接等需要显式关闭的资源时。
基本用法:
try-with-resources 语句需要在 try 关键字后面定义一个或多个实现了 AutoCloseable 接口的资源。资源将在 try 块执行完毕后自动关闭,无论 try 块中是否抛出了异常。这使得资源管理变得更加简单和安全。

语法结构:

try (ResourceType resource = new ResourceType()) {
    // 使用资源的代码
} catch (Exception e) {
    // 异常处理代码
}
// 资源会在 try 块结束后自动调用 close() 关闭,无需 finally 块
6、异常使用有哪些需要注意的地方? ⭐⭐⭐
  • 不要定义静态异常变量:静态异常会共享堆栈信息,掩盖真实错误位置。
  • 不要复用异常对象:每次抛出异常时必须 new 新对象,否则堆栈信息会指向首次创建位置,导致调试困难。
  • 异常信息应具体且有意义:包含上下文信息(如参数值、操作类型),避免模糊描述。建议抛出更加具体的异常而不是其父类
  • 避免重复记录日志:若已在捕获处记录完整日志,再次抛出时无需重复记录。
  • finally块注意:避免在finally中抛出异常会覆盖原始异常。,避免在 finally 中 return会覆盖 try/catch 的返回值。

1.5、其他高级特征

1、什么是泛型?有什么作用?⭐⭐⭐⭐

泛型是 Java 中的一种机制,它允许在类、接口和方法中使用类型参数,从而提高代码的重用性和类型安全性。泛型是在 Java 5 中引入的,它使得代码更加灵活和安全,同时也能减少强制类型转换的需求。

  • 类型安全:泛型提供了编译时的类型检查,防止了类型转换错误。例如,在泛型集合中添加非预期类型的元素会在编译时被检测到,从而避免了运行时的ClassCastException。
  • 代码重用:使用泛型可以编写通用的算法和数据结构,而不必为每种类型编写重复的代码。例如,Java 标准库中的 List, Map, Set等集合类都使用了泛型,使得它们可以存储不同类型的对象。
  • 消除强制类型转换:在没有泛型的情况下,你可能需要使用强制类型转换来将对象从 Object类型转换为特定类型。泛型避免了这种强制转换,使得代码更清晰、安全。
2、什么是反射及其优缺点? ⭐⭐⭐⭐

反射允许程序在运行时动态获取类的信息(如字段、方法、注解),并操作对象的属性和行为。
反射的优点:

  • 灵活性:允许在运行时动态操作类、方法和字段,使得代码能够适应不断变化的需求。例如,可以在不知道具体类的情况下操作它们。
  • 简化框架的实现:许多框架和库(如 Spring 和 Mybatis)使用反射来实现配置和动态行为,从而使得框架的使用更加灵活和简洁。
  • 动态创建对象:在一些动态语言或需要动态配置的系统中,反射可以用来创建对象、调用方法,而无需在编译时知道所有信息。
  • 测试和调试:反射可以用来访问和测试私有字段和方法,这对于单元测试和调试很有帮助。

反射的缺点:

  • 性能开销:反射涉及到大量的动态检查和操作,这会比正常的静态类型操作要慢。频繁使用反射可能会导致性能下降。
  • 安全问题:反射可以访问私有字段和方法,这可能导致安全问题。如果不加以控制,可能会泄露敏感信息或破坏对象的封装性。
  • 编译时检查失效:使用反射时,很多错误(如方法不存在、字段不匹配等)只能在运行时发现。这意味着程序的类型安全性会降低,可能导致难以发现和调试的问题。
  • 代码复杂性:过度使用反射可能导致代码变得复杂和难以维护。反射代码通常较难理解,因为它绕过了静态类型检查和编译时验证。
3、什么是注解,注解的解析方法有哪几种?⭐⭐

注解是 Java 1.5 引入的一种元数据(Metadata) 机制,用于为代码添加描述性信息。它不直接影响程序逻辑,但可被编译器、框架或运行时环境读取。实现以下功能:编译时检查(如 @Override 确保方法正确重写);代码生成(如 Lombok 自动生成 getter/setter);框架配置(如 Spring 的 @Component 标记组件);文档生成(如 @Deprecated 标记废弃接口)。

常见的解析方法有三种:

  • 编译期解析:编译器在编译 Java 代码的时候扫描对应的注解并处理,如使用@Override
    注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期解析:在程序运行时,通过反射 API 动态获取对象 / 方法 / 字段的注解,执行对应逻辑。如Spring 通过 @Autowired 实现依赖注入;
  • 类加载期解析:例如:Spring AOP 通过 @Aspect生成代理类
4、序列化和反序列化 ⭐⭐⭐⭐⭐

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

  • 序列化:将数据结构或对象转换成二进制字节流的过程
  • 反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
  • 综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
  • 对于不想进行序列化的变量,使用 transient 关键字修饰。
5、为什么不推荐使用 JDK 自带的序列化? ⭐⭐⭐
  • 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

  • 性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

  • 存在安全问题:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。

6、IO流为什么要分为字节流和字符流⭐⭐⭐
  • 数据类型的针对性:字节流处理二进制数据,字符流处理文本数据。

  • 编码处理的自动化:字符流自动处理字符编码,避免手动转换的复杂性和错误。

  • 性能优化:字符流针对文本操作进行了优化(如缓冲、按行读取)。

7、BIO,NIO,AIO区别 ⭐⭐⭐⭐⭐
  • BIO(Blocking I/O):即传统的阻塞式 I/O。在 BIO 中,当线程执行 I/O操作时,如读取文件或网络数据,线程会被阻塞,直到操作完成。这意味着在 I/O 操作进行期间,线程无法执行其他任务,会一直处于等待状态。
  • NIO(Non - Blocking I/O):也叫新 I/O 或非阻塞式 I/O。NIO 允许线程在执行 I/O操作时不会被阻塞。线程可以在 I/O 操作未完成时继续执行其他任务,通过轮询或事件通知的方式来获取 I/O 操作的结果。
  • AIO(Asynchronous I/O):即异步 I/O。AIO 与 NIO 的非阻塞不同,它是基于事件和回调机制实现的。当发起一个I/O 操作后,线程会继续执行其他任务,I/O 操作完成后会通过回调函数来通知线程,线程不需要主动去查询 I/O 操作的状态。

二、Java集合

Java 集合,也叫作容器,主要是由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。对于Collection 接口,下面又有三个主要的子接口:List、Set 、 Queue。

在这里插入图片描述

2.1、集合概述

1、集合框架底层数据结构 ⭐⭐⭐⭐⭐

List(有序、可重复)

  • ArrayList:Object[] 数组。线程不安全,支持快速随机访问。
  • Vector:Object[] 数组。线程安全(方法用synchronized修饰),但性能差,已淘汰。
  • LinkedList:双向链表。线程不安全,插入删除高效,随机访问慢。
  • CopyOnWriteArrayList:Object[] 数组。线程安全,写操作复制新数组,适合读多写少场景。

Set(无序,唯一)

  • HashSet:基于HashMap实现,元素作为HashMap的key存储(value为固定PRESENT对象)。线程不安全。
  • LinkedHashSet:继承HashSet,内部通过LinkedHashMap实现,维护插入顺序。线程不安全。
  • TreeSet:基于TreeMap(红黑树)实现,元素按自然顺序或Comparator排序。线程不安全。

Queue(有序,可重复)

  • PriorityQueue:Object[] 数组实现小顶堆。线程不安全,元素按自然顺序或Comparator排序。
  • ArrayDeque:可扩容动态双向数组。线程不安全,高效实现栈和队列操作。

Map(key唯一,value可重复)

  • HashMap:JDK8+为数组+链表/红黑树,链表长度≥8且数组长度≥64时树化。线程不安全。
  • LinkedHashMap:继承HashMap,通过双向链表维护插入顺序或访问顺序。线程不安全。
  • ConcurrentHashMap:JDK8+采用数组+链表/红黑树,CAS+synchronized实现线程安全,锁粒度更细。
  • Hashtable:数组+链表,全表锁,线程安全但已淘汰,建议用ConcurrentHashMap替代。
  • TreeMap:红黑树实现,key按自然顺序或Comparator排序。线程不安全。
2、Comparable 和 Comparator 的区别 ⭐⭐⭐

Comparable 是类内部实现的自然排序,Comparator 是外部定义的自定义排序。
Comparable 接口

  • 定义: Comparable 接口用于定义对象的自然排序。实现此接口的类需要重写 compareTo 方法。
  • 比较方式: compareTo 方法只允许以当前对象与另一个对象进行比较。一般来说,该对象的排序顺序在类的实现中是固定的。
  • 实现类: 通常在类中实现此接口,例如,如果你想让 Person 类根据年龄进行排序,你可以在 Person 类中实现 Comparable
    接口。
public class Person implements Comparable<Person> {
    private int age;

    public Person(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Person other) {
        return Integer.compare(this.age, other.age);
    }
}

Comparator

  • 定义: Comparator 接口用于定义一个比较器,可以在外部实现以定义多个不同的排序方式。实现此接口的类需要重写 compare方法。
  • 比较方式: compare方法可以接受两个对象进行比较,允许你在多个比较器之间选择不同的排序规则,也就是说,你可以为同一类型的对象定义多种排序方式。
  • 实现类: 通常在一个单独的类中实现,例如,你可以创建一个 AgeComparator 类来根据年龄排序,一个 NameComparator类来根据名字排序。
import java.util.Comparator;

public class AgeComparator implements Comparator<Person> {
    @Override
    public int compare(Person p1, Person p2) {
        return Integer.compare(p1.getAge(), p2.getAge());
    }
}

2.2、List集合

1、ArrayList 和 Array(数组)的区别? ⭐⭐⭐

ArrayList 内部基于动态数组实现,比 Array(静态数组) 使用起来更加灵活:

  • 长度可变性:ArrayList会根据实际存储的元素动态地扩容或缩容,而 Array 被创建之后就不能改变它的长度了。
  • 类型安全:ArrayList 允许你使用泛型来确保类型安全,Array 则不可以。
  • 存储内容:ArrayList 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。Array可以直接存储基本类型数据,也可以存储对象。
  • 功能支持:ArrayList 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 add()、remove()等。Array只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。
  • 创建方式:ArrayList创建时不需要指定大小,而Array创建时必须指定大小。
2、如何实现数组和List之间的转换 ⭐⭐⭐
  • 数组 → List
    Arrays.asList(array) 快速转换,但返回的 固定长度 List(底层共享原数组)。
    相互影响:修改元素值会同步到原数组,但增删元素会抛异常。
    new ArrayList<>(Arrays.asList(array))
    创建独立 List,与原数组无关联,支持增删操作。
  • List → 数组
    list.toArray(new T[0])
    返回独立数组,与原 List 无关联,修改互不影响。
3、ArrayList 可以添加 null 值吗? ⭐⭐⭐

ArrayList 中可以存储任何类型的对象,包括 null 值。不过,不建议向ArrayList 中添加 null 值, null 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。

4、ArrayList 和LinkedList插入和删除元素的时间复杂度?⭐⭐⭐⭐

它们的性能差异主要源于底层数据结构:

  • ArrayList 基于数组,尾部插入/删除在无需扩容时是 O(1)(扩容时为 O(n)),而头部或中间操作需移动元素,时间复杂度O(n)。

  • LinkedList 基于双向链表,头尾插入/删除直接操作指针(O(1)),但中间操作需遍历链表,时间复杂度 O(n)。因此,LinkedList 更适合频繁头尾操作,而 ArrayList 适合随机访问和尾部操作。”

5、ArrayList和LinkedList有什么区别 ⭐⭐⭐⭐⭐

ArrayList和LinkedList是)java集合框架中List接口的两个常见实现类,它们在底层实现和性能特点上有以下几点区别:
1).底层数据结构:ArrayList使用动态数组来存储元素,而LinkedList使用双向链表来存储元素。
2).随机访向性能:Arraylist支持高效的随机访问,因为它可以通过下标计算元素在数组中的位置。而Linkedlst在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
3)、插入和删除性能:ArayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
4).内存占用:ArrayList底层是数组,内存连续,节省内存,而LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
5)、扩展性:ArrayList 在元素超过当前容量时,需要创建一个更大的数组并复制原有元素,可能会造成性能开销。LinkedList 可以方便地在任意位置插入元素,不需要移动其他元素。
综上所述,当频繁访问元素时,使用 ArrayList 更高效,当频繁插入和删除操作时,使用 LinkedList 更合适。

6、ArrayList底层的实现原理是什么/扩容机制 ⭐⭐⭐⭐⭐

1. 底层数据结构
ArrayList 底层基于 Object[] 数组 实现,是一个动态数组,支持自动扩容。
2. 初始容量
无参构造:初始容量为 0(实际是一个空数组),第一次添加元素时扩容为默认容量 10。
指定容量构造:例如 new ArrayList(100),直接初始化为指定容量(100)。
3. 扩容机制
扩容条件:当添加元素后,数组已使用长度(size+1)超过当前数组容量时触发扩容。
扩容规则:新容量为原容量的 1.5 倍(源码:newCapacity = oldCapacity + (oldCapacity >> 1))。
扩容代价:需通过 Arrays.copyOf 将旧数组数据拷贝到新数组,频繁扩容会影响性能。
4. 添加元素流程
容量检查:确保当前容量足够存入新元素(size+1 > capacity)。
触发扩容:若不足,按 1.5 倍扩容并拷贝数据。
插入元素:将新元素放入数组 size 位置,size 自增 1。
返回结果:返回 true 表示添加成功。

2.3、Set集合

1、比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同⭐⭐⭐⭐⭐
特性 HashSet LinkedHashSet TreeSet
底层数据结构 哈希表 哈希表 + 双向链表 红黑树
顺序性 无序 插入顺序 自然/自定义排序
性能 O(1) 查找/插入 略低于 HashSet O(log n) 操作
是否允许 null ❌(需排序无法比较)
线程安全
适用场景 快速去重、无需顺序 需保留插入顺序的去重 需排序、范围查询

2.4、Map集合

1、HashMap底层的实现原理 ⭐⭐⭐⭐⭐
  • 数据存储:HashMap底层采用哈希表结构,通过key的哈希值计算数组下标,若哈希冲突,则同一位置的元素以链表或红黑树存储。
  • 扰动算法:JDK1.8通过高位参与运算(key.hashCode()高16位异或低16位)减少哈希碰撞。
  • 版本差异:JDK1.8前:数组+链表,冲突时链表插入头部(头插法)。JDK1.8后:数组+链表+红黑树,当链表长度超过8且数组长度≥64时,链表转为红黑树(提高查询效率);红黑树节点数≤6时退化为链表。冲突时插入链表尾部(尾插法)
2、HashMap的put流程 ⭐⭐⭐⭐⭐
  • 初始化检查:若数组(table)为空,先调用resize()初始化(默认容量16,阈值12)。
  • 计算下标:通过key计算hash值确定键值对存放的桶位置。
  • 处理空桶:若当前桶无数据(table[i]==null),直接新建节点放入。
  • 处理冲突:若桶已有数据:Key相同:若头节点key与待插入key相同(equals为真),直接覆盖value。
    红黑树插入:若桶为红黑树结构,调用putTreeVal()插入或更新。
    链表遍历:遍历链表,若无相同key则在尾部插入新节点。插入后若链表长度≥8,触发树化检查(需数组长度≥64才转为红黑树)。若有相同key则直接覆盖value
  • 扩容判断:插入成功后,若总键值对数超过阈值(size > threshold),调用resize()扩容。
3、HashMap的扩容机制 ⭐⭐⭐⭐⭐
  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍;
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    如果是红黑树,走红黑树的添加
    如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,为0的话该元素的位置停留在原始位置,不为0的话移动到原始位置+增加的数组大小这个位置上
4、HashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐
  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable内部的方法基本都经过synchronized 修饰。(如果要保证线程安全的话就使用 ConcurrentHashMap ); 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2的幂次方大小(HashMap 中的tableSizeFor()方法保证)。也就是说 HashMap 总是使用 2的幂作为哈希表的大小,
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable没有这样的机制。
  • 哈希函数的实现:HashMap 对哈希值进行了高位和低位的混合扰动处理以减少冲突,而 Hashtable 直接使用键的hashCode()值。
5、HashMap 和 TreeMap 区别 ⭐⭐⭐⭐⭐
  • 1、底层数据结构: HashMap:采用哈希表作为底层数据结构,通过哈希函数将键映射到桶中,存放键值对。 TreeMap:基于红黑树实现,能够保持键的排序。

  • 2、顺序: HashMap:不保证元素的顺序,插入的顺序可能会被打乱。 TreeMap:根据键的自然顺序(或根据比较器提供的顺序)进行排序,因此迭代时会按照键的顺序返回元素。

  • 3、性能: HashMap:在大多数情况下,插入、删除和查找的时间复杂度为 O(1),但最坏情况下为 O(n)(当发生哈希冲突严重时)。 TreeMap:插入、删除和查找的时间复杂度为 O(log n),因为需要维护树的平衡性。

  • 4、键的要求: HashMap:键可以为 null,最多只能有一个 null 键。 TreeMap:不允许使用 null 作为键,因为其需要比较键的大小。

  • 5、用途: HashMap:适合对元素的顺序没有要求,主要用于快速访问。 TreeMap:适合需要排序或者范围查询的场景。
    总结来说,如果对元素的顺序没有要求并且希望获得更好的性能,使用 HashMap。如果需要对元素进行排序并能接受更高的操作复杂度,使用 TreeMap。

6、HashMap 的长度为什么是 2 的幂次方 ⭐⭐⭐⭐⭐
  • 高效位运算替代取模:
    哈希计算下标时,公式为 (n-1) & hash(n 为容量)。
    若 n 是 2 的幂,n-1 的二进制全为 1(如 n=16 → 15=0b1111),此时 & 操作等价于 hash % n,但位运算效率远高于取模。
  • 哈希分布更均匀:
    2 的幂次方的容量设计,配合扰动函数(hash = key.hashCode() ^ (hash >>> 16)),能让哈希值的高位参与下标计算,减少哈希碰撞。
  • 扩容便捷性:
    每次扩容容量翻倍(仍保持 2 的幂),旧数据迁移时只需判断 hash & oldCap 是否为 0,即可快速分配到新位置(原位置或原位置+旧容量),无需重新计算所有哈希。
  • 避免空间浪费:
    若容量非 2 的幂,用户自定义初始容量时可能导致实际分配的容量与预期不符(需通过 tableSizeFor() 方法修正为最近的 2 的幂),可能浪费内存。
7、HashMap 多线程操作导致死循环问题 ⭐⭐⭐
  • JDK1.7 及之前版本的 HashMap在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。
  • 为了解决这个问题,JDK1.8 版本的 HashMap采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。
  • 但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用ConcurrentHashMap 。
8、HashMap 为什么线程不安全? ⭐⭐⭐⭐⭐

核心原因:HashMap 的设计未考虑多线程并发操作,导致以下问题:

  • 多线程扩容导致死循环(JDK1.7 及之前)
    原因:多线程扩容时,头插法可能导致链表反转,形成环形链表(如线程 A 和 B同时扩容并修改节点引用)。
    结果:后续操作遍历链表时陷入死循环,CPU 飙升。
    JDK1.8 改进:改用尾插法,但线程不安全问题依然存在(只是降低了死循环概率)。
  • 多线程扩容导致数据丢失
    场景:多线程同时插入数据,触发哈希冲突。
    结果:若两个线程计算相同下标,后插入的键值对可能覆盖前一个线程的数据。
  • 多线程扩容导致数据丢失
    场景:多线程同时触发扩容(resize)。
    结果:链表或红黑树结构被破坏,部分数据丢失或引用混乱。
  • 迭代导致抛出异常
    场景:一个线程迭代遍历时,另一个线程修改结构(如增删)。
    结果:迭代器的 modCount 与预期值不一致,抛出异常(即使单线程也可能触发,但多线程更不可控)。
9、ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 ⭐⭐⭐⭐⭐
  • JDK1.7(分段锁)
    数据结构:由多个 Segment 组成,每个 Segment 是一个独立的哈希表。
    并发控制:每个 Segment用 ReentrantLock 加锁,不同 Segment 可并发操作。
    缺点:并发度固定,扩容仅限于单个 Segment。
  • JDK1.8(CAS + synchronized)
    数据结构:数组 + 链表/红黑树,Node 的 val 和 next 用 volatile 保证可见性。
    并发控制:无锁读:直接访问 volatile 变量。CAS 写:空桶用 CAS 插入;非空桶锁头节点(synchronized)。多线程扩容:线程插入时若发现扩容,协助迁移数据。
    优点:锁粒度更细,并发度动态提升。

总结
ConcurrentHashMap 通过分段锁(JDK 1.7 及之前版本)和基于 CAS 操作的无锁算法(JDK 1.8 及之后版本)来实现线程安全,从而在高并发环境下提供了更好的性能。读操作通常无锁,写操作使用 CAS 操作或加锁来确保线程安全。ConcurrentHashMap 的设计使得它能够在多线程环境下安全高效地操作。

10、ConcurrentHashMap 为什么 key 和 value 不能为 null?⭐⭐⭐⭐⭐
  • 二义性问题: get(key) 返回 null 时,无法区分是“键不存在”还是“值本身为 null”。 并发场景下,containsKey和 get 之间的修改可能导致误判。
  • 简化并发设计: 避免处理 null 的特殊逻辑,减少代码复杂度。
  • 与 Hashtable 一致性: 延续 Hashtable 的设计原则,强制显式处理缺失键值。
11、ConcurrentHashMap 能保证复合操作的原子性吗? ⭐⭐⭐
  • ConcurrentHashMap本身是线程安全的,但它不能自动保证复合操作的原子性。例如‘检查后更新’操作,在并发环境下可能导致竞态条件。

  • 为了保证原子性,ConcurrentHashMap 提供了以下内置方法: putIfAbsent():不存在则插入。 compute()/ computeIfAbsent():根据 key 计算新值。 merge():合并新旧值。 这些方法通过内部锁或 CAS 操作保证了原子性,避免了手动实现复合操作的风险。

12、ConcurrentHashMap 和 Hashtable 的区别 ⭐⭐⭐⭐⭐
特性 ConcurrentHashMap Hashtable
并发机制 JDK1.7分段锁;JDK1.8 CAS+桶锁 全局锁(所有方法加 synchronized)
性能 高并发下高效 高并发下性能差
迭代器 强一致性(不抛异常) 弱一致性(可能抛异常)
初始容量 16(2的幂) 11
扩容规则 翻倍(2的幂) 2n+1
Null支持 键值均不能为null 键值均不能为null
版本 JDK1.5+ JDK1.0+

核心区别:
锁粒度:ConcurrentHashMap 细粒度锁(段或桶),Hashtable 全局锁。
设计目标:ConcurrentHashMap 为高并发优化,Hashtable 已淘汰。

三、Redis

3.1、概述

1、什么是Redis? ⭐⭐⭐⭐

基于内存的键值(Key-Value)数据库系统。常用作缓存、会话存储、分布式锁、消息队列、计数器和限流、排行榜等。

  • 缓存:将数据库的热点数据放在内存中,减少数据库压力,提升读取速度。
  • 会话存储:将用户登录会话集中存储在 Redis 中,实现分布式服务的会话共享
  • 分布式锁:在分布式系统中协调多节点对共享资源的互斥访问。
  • 消息队列:利用 List(阻塞操作)、Streams(5.0+)实现轻量级消息队列。
  • 计数器和限流:利用 INCR命令实现点赞数、播放量等;结合过期时间实现限流
  • 排行榜:使用 ZSET(有序集合)实现按分数排序的榜单。
2、Redis为什么快? ⭐⭐⭐⭐⭐
  • 单线程模型:避免上下文切换和锁竞争,通过IO多路复用监听多个Socket事件
  • 内存操作:数据全存内存,读写速度远高于磁盘
  • 高效数据结构:如跳表、压缩列表等,优化内存和查询效率
3、解释一下I/O多路复用模型? ⭐⭐⭐⭐
  • I/O多路复用是一种高效的网络通信模型,它允许单个线程同时监听多个Socket连接,并在其中任何一个Socket可读或可写时得到通知,从而避免无效等待,提高CPU利用率。
  • 目前的I/O多路复用都是采用的epoll模式实现,epoll会在通知Redis服务Socket就绪的同时,把已就绪的Socket信息直接传递给Redis服务,避免了遍历所有Socket的开销,大幅提升了性能。"
  • 在Redis6.0之后,为了提升更好的性能,多线程处理网络I/O(解析请求/发送回复),单线程执行命令:保证原子性,避免锁竞争 这种设计既提升吞吐量,又维持了Redis的线程安全特性。
4、Redis的常用数据结构 ⭐⭐⭐⭐⭐

3.2、缓存

1、什么是Redis的缓存穿透,怎么解决 ⭐⭐⭐⭐⭐

缓存穿透是指查询一个既不在缓存也不在数据库中的数据,导致每次请求都直接访问数据库,从而造成数据库压力过大甚至宕机的问题。这种情况通常由恶意攻击或无效请求(如不存在的ID)引发。
缓存穿透的解决方案

  • 空值缓存(缓存空对象)
    原理:当数据库查询结果为空时,将空值(如key-null)写入缓存,并设置较短的过期时间(如30秒)。后续相同请求会命中空值缓存,避免重复访问数据库。
    优点:实现简单,能有效拦截短时间内的高频无效请求。
    缺点:短时间内存占用增加。若后续数据库新增了该数据,可能导致缓存与数据库的短期不一致
  • 布隆过滤器
    原理:它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。
    请求到达时,先通过布隆过滤器判断数据是否存在:
    若返回“不存在”,直接拦截请求。
    若返回“可能存在”,继续查询缓存和数据库。
    优点:内存占用极小,适合大规模数据场景。
    缺点:存在误判率(可能将不存在的误判为存在,但不会漏判)。需在数据写入时同步更新布隆过滤器
2、什么是Redis的缓存击穿,怎么解决 ⭐⭐⭐⭐⭐

Redis的缓存击穿是指某个热点数据在缓存中过期(或失效)的瞬间,大量并发请求直接穿透缓存层访问数据库,导致数据库压力骤增甚至崩溃的现象。这种情况通常发生在高并发场景下,例如秒杀商品、热门新闻等热点数据突然失效时。
解决方案

  • 互斥锁(分布式锁)
    原理:当缓存失效时,仅允许一个线程查询数据库并重建缓存,其他线程等待或重试。例如通过Redis的SETNX命令实现锁机制。
    优点:强一致性,确保数据最新。
    缺点:性能下降(线程需等待锁),可能引发死锁或超时问题
  • 逻辑过期(逻辑删除)
    原理:缓存中不设置物理过期时间(TTL),而是将过期时间写入数据值中。当发现逻辑过期后,异步更新缓存。
    优点:高可用性,避免线程等待,性能高。
    缺点:短期数据不一致,需容忍旧数据
3、什么是Redis的缓存雪崩,怎么解决 ⭐⭐⭐⭐⭐

Redis的缓存雪崩是指在同一时间段内,大量缓存数据集中过期失效或缓存服务器宕机,导致所有请求直接涌向后端数据库,造成数据库压力激增甚至崩溃的现象。
解决方案

  • 随机化过期时间:为每个Key的TTL添加随机值(例如基础时间±随机分钟),分散过期时间点,避免同一时段大量数据失效
  • 热点数据永不过期:对高频访问的数据(如首页推荐商品),设置逻辑过期而非物理过期,通过异步线程定期更新数据,保证缓存始终可用
  • Redis高可用部署:采用主从复制(Master-Slave)、哨兵模式(Sentinel)或集群模式(Cluster)实现故障自动转移,确保单点故障不影响整体服务
  • 多级缓存架构:结合本地缓存与分布式缓存(Redis),形成多级缓存屏障。例如,本地缓存应对高频请求,Redis作为二级缓存分担压力
  • 熔断与限流策略:在网关或服务层设置限流(如令牌桶算法),限制数据库访问QPS;当请求量超过阈值时触发熔断,直接返回降级内容(如默认页面)(这个缓存三兄弟都可以说)
4、mysql的数据如何与Redis同步呢(双写一致性) ⭐⭐⭐⭐⭐

方案1: Cache-Aside Pattern(旁路缓存模式)

读流程:1)读缓存,命中则返回。2)缓存未命中,则读数据库。3)将数据库数据写入缓存。
写流程:1)更新 MySQL 中的数据。2)删除 Redis 中对应的缓存。

优点:

  • 简单高效:删除操作是幂等的,即使失败也相对安全。
  • 并发问题概率低:即使出现不一致,时间窗口也很短

缺点:极端并发场景下仍可能不一致:

  • 时刻1:缓存刚好失效。
  • 时刻2:请求A读缓存,未命中,去读数据库(得到旧值)。
  • 时刻3:请求B更新数据库。
  • 时刻4:请求B删除缓存。
  • 时刻5:请求A将读到的旧值写入缓存。
  • 结果:缓存中变成了旧数据。

改进:延迟双删

  • 删除 Redis 缓存。
  • 更新 MySQL 数据库。
  • 休眠一段时间(如 500ms - 1s,具体时间需要根据业务读耗时评估)。
  • 再次删除 Redis 缓存。
  • 作用:第二次删除的目的是清除在「更新数据库」和「第一次删缓存」之间,可能被其他请求写入的脏数据。

方案2、MQ或者Canal中间件异步通知
MQ
步骤1:数据库更新后发送消息。业务代码在更新 MySQL 后,向 MQ(如 Kafka、RabbitMQ)发送一条消息,携带数据标识及操作类型(如删除缓存或更新缓存)。
步骤2:消费者异步处理缓存。独立服务监听 MQ 消息,解析后执行缓存操作(如删除或更新 Redis 数据)
Canal
步骤1:监听 MySQL Binlog。Canal 伪装为 MySQL 从库,实时解析主库的 Binlog 日志,捕获数据变更事件(如 INSERT、UPDATE)。
步骤2:发送变更消息到 MQ。将解析后的变更数据(如表名、主键 ID、新值)发送到 MQ(如 Kafka)。
步骤3:消费者更新缓存。消费者从 MQ 读取消息,根据操作类型删除或更新 Redis 数据。

方案3:分布式锁

  • 写请求到来时,先获取一个分布式锁(基于 Key)。
  • 获取锁后,进行「更新数据库 -> 删除缓存」的操作。
  • 读请求在缓存未命中时,也尝试获取同一个锁。如果获取不到,说明有写操作在进行,则等待或重试。
  • 操作完成后释放锁。
  • 效果:通过锁将并发的读写操作串行化,避免了所有并发问题。

缺点:
性能损耗巨大,完全丧失了缓存的高并发优势。
复杂度高,容易产生死锁。

5、redis做为缓存,数据的持久化是怎么做的 ⭐⭐⭐⭐⭐
  • RDB:定期将 Redis 内存中的数据生成 二进制快照文件(dump.rdb)。
    可通过 SAVE(阻塞式)或 BGSAVE(后台异步)手动触发。
    优点:
    恢复速度快(二进制格式,文件小)。
    适合大规模数据备份(如每天全量备份)。
    对性能影响小(子进程处理,主进程不阻塞)。
    缺点:
    可能丢失数据(最后一次快照后的修改会丢失)。
    大数据量时 fork 可能短暂阻塞 Redis(取决于内存大小)。
  • AOF:记录所有写操作命令(文本格式),类似 MySQL 的 binlog。
    重启时 重放 AOF 日志 恢复数据。
    优点
    数据安全性高(最多丢失1秒数据)。
    支持日志重写(BGREWRITEAOF 压缩冗余命令)。
    可读性强(可用于数据审计)。
    缺点:
    文件体积通常比 RDB 大(需定期重写优化)。
    恢复速度较慢(需逐条执行命令)。

推荐组合使用 RDB + AOF:RDB 用于快速恢复和备份、AOF 确保数据安全性

6、Redis的数据过期策略 ⭐⭐⭐⭐

在redis中提供了两种数据过期删除策略。

  • 第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它,返回 nil;反之,返回该key。对 CPU 友好,只处理被访问的 key,但可能导致大量已过期但未被访问的 key 堆积
  • 第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。默认每秒 10 次。可以减少内存浪费,但CPU 开销比惰性删除大
    Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用
7、Redis的数据淘汰策略 ⭐⭐⭐⭐

1)不淘汰(默认)
策略名:noeviction
行为:当内存不足时,新写入操作会报错(如 OOM),拒绝所有可能增加内存的请求。
适用场景:数据不允许丢失(例如关键业务数据),需严格避免内存超限。

2)淘汰设置了过期时间的键
策略名:
volatile-lru:淘汰最近最少使用(LRU)的键(仅针对有过期时间的键)。
volatile-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,针对有过期时间的键)。
volatile-ttl:淘汰存活时间最短(TTL 最小)的键。
volatile-random:随机淘汰任意有过期时间的键。
适用场景:内存中同时存在永久数据和临时缓存数据,需优先淘汰缓存。

3)淘汰所有键(不区分是否设置过期时间)
策略名:
allkeys-lru:淘汰最近最少使用(LRU)的键(所有键)。
allkeys-lfu:淘汰最不经常使用(LFU)的键(Redis 4.0+,所有键)。
allkeys-random:随机淘汰任意键。
适用场景:内存中所有数据都可被淘汰(例如纯缓存场景)。

8、什么是Redis的大Key和热Key问题?如何解决? ⭐⭐⭐⭐⭐

3.3、分布式锁

1、Redis分布式锁如何实现? ⭐⭐⭐⭐⭐

在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
使用现成的库,Redisson 是一个高级的 Java Redis 客户端,它封装了复杂的逻辑(如 Lua 脚本、看门狗机制),提供了简单易用的 API。

2、如何控制Redis实现分布式锁的有效时长呢 ⭐⭐⭐⭐

Redis 的 SETNX 指令确实难以直接控制锁的有效时长,但可以通过 Redisson 框架优化实现,其核心逻辑如下:

  • 看门狗自动续期机制:如果业务未执行完但锁即将过期,Redisson的看门狗会每隔一段时间检查锁是否仍被持有。若持有,则自动延长锁的失效时间,避免锁提前释放。
  • 释放锁与流程闭环:业务执行完成后,手动释放锁即可,确保锁资源及时回收。
  • 自旋锁提升并发性能:在高并发场景下,若客户1持有锁,客户2不会立即被拒绝,而是以自旋(循环尝试)的方式等待锁释放。一旦客户1释放锁,客户2能立即获取,减少阻塞时间,提升系统吞吐量。
3、Redisson实现的分布式锁是可重入的吗?⭐⭐⭐⭐

是可重入的。这样做是为了避免死锁的产生。
第一次加锁时,记录你的线程ID和持有锁的次数(计数器=1)。
第二次加锁时,发现是同一个线程,计数器+1(变成2),直接放行。
每次解锁时,计数器-1,直到计数器归零才真正释放锁。

4、Redisson实现的分布式锁能解决主从一致性的问题吗? ⭐⭐⭐

这个是不能的。

Redisson 分布式锁的默认实现(主从场景风险)
依赖 Redis 主节点:Redisson 的 RLock 默认通过 Redis 主节点加锁(SET + Lua 脚本)
主从异步复制问题:
当主节点加锁成功后,若数据未同步到从节点时主节点崩溃
从节点晋升为新主节点后,锁状态丢失,导致多个客户端同时获取锁

Redisson 的解决方案:RedLock 算法:
向多个独立 Redis 节点(通常 ≥3,且主从部署在不同机器)发起加锁请求
当大多数节点(N/2 +1)加锁成功时,视为全局加锁成功
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。

3.4、集群

1、介绍一下Redis主从同步 ⭐⭐⭐⭐

单节点Redis的并发能力是有上限的,要进一步提高Redis的高并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
主从同步分为 全量同步增量同步 两个阶段,核心目标是确保主从节点数据一致性。

  1. 全量同步(首次建立连接时触发)
    触发条件:从节点首次连接主节点,或主从 replication id 不匹配。
    流程:
    1)从节点发起请求:携带自身 replication id 和 offset(偏移量)。
    2)主节点校验:若 replication id 不同,判定为首次同步。主节点将自己的 replication id 和 offset 发送给从节点,达成元数据一致。
    3)生成并传输 RDB 快照:主节点执行 BGSAVE,生成 RDB 文件(内存数据快照)。
    发送 RDB 文件到从节点,从节点清空旧数据后加载 RDB。
    4)同步缓冲命令:主节点在生成 RDB 期间,将新写入命令记录到 缓冲区(repl_backlog)。RDB 传输完成后,主节点将缓冲区命令发送给从节点执行,确保数据完全一致。
  2. 增量同步(从节点重启或断线重连时触发)
    触发条件:主从 replication id 一致,但 offset 落后。
    流程:
    1)从节点上报 offset:发送当前 replication id 和 offset。
    2)主节点推送差异数据:从缓冲区(repl_backlog)中找到从节点 offset 之后的命令。发送这些命令到从节点执行,追平数据差异。
2、怎么保证Redis的高并发高可用? ⭐⭐⭐⭐⭐
  • 主从集群(读写分离) 作用:
    高并发:主节点处理写操作,从节点分担读请求,提升整体吞吐量。
    数据冗余:主节点数据同步到从节点,实现数据备份。
  • 哨兵模式(故障自动恢复) 核心功能:
    监控:持续检查主从节点的健康状态。
    自动故障转移:主节点(Master)宕机时,哨兵(Sentinel)选举一个从节点(Slave)晋升为新主节点。原主节点恢复后,自动降级为从节点并同步新主节点数据。
    服务发现:客户端通过哨兵获取最新主节点地址,故障转移后自动切换连接。
3、Redis集群脑裂,该怎么解决呢?⭐⭐⭐⭐⭐
  • 脑裂场景: 网络分区导致 Sentinel 误判原主节点(old master)宕机,触发故障转移,选举从节点(slave)为新主节点。此时客户端仍可能向原主节点写入数据,但新主节点无法同步这些数据。
    后果:网络恢复后,原主节点降级为从节点并清空数据,导致写入原主节点的数据永久丢失。
  • 在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。
4、Redis的分片集群有什么作用? ⭐⭐⭐⭐⭐

分片集群主要解决的是海量数据存储的问题

  • 集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。
  • 同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
  • Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。取值的逻辑是一样的。

四、MySQL

4.1、慢查询与优化

1、MySQL 中如何定位和分析慢查询? ⭐⭐⭐

第一步:定位慢查询

  1. 业务 / 应用层定位(运维监控系统)
    工具:像Skywalking、Prometheus + Grafana这样的APM或监控系统。
    操作:监控接口响应时间(如阈值设为 2 秒),通过 APM 的链路追踪功能,定位接口中耗时的具体环节(如 SQL 执行、第三方调用、代码逻辑)。
    优势:能快速关联业务场景(如哪个接口、哪个用户操作触发),便于理解慢查询的业务上下文。
  2. 数据库层定位(MySQL 慢查询日志)
    开启方式:在MySQL配置文件(如my.cnf)中设置。
slow_query_log = 1                  # 开启慢查询日志(1开启,0关闭)
slow_query_log_file = /var/lib/mysql/slow.log  # 日志存储路径(需MySQL进程有写入权限)
long_query_time = 2                 # 慢查询阈值,单位秒(默认10秒,可设为0.1监控所有SQL)
log_queries_not_using_indexes = 1   # 记录未使用索引的SQL(即使未超阈值,建议开启)

分析工具:直接查看日志文件很困难,推荐使用MySQL自带的mysqldumpslow工具,或者更强大的pt-query-digest(Percona Toolkit的一部分)来汇总和分析慢日志,它能帮你快速找到“最耗时”、“执行次数最多”的SQL。

第二步:分析慢查询
找到具体的慢SQL后,使用 EXPLAIN 命令来查看其执行计划。重点关注以下几个关键字段:

  • type(访问类型):这是判断查询效率的首要指标。它显示了MySQL决定如何查找表中的行。
    从优到劣:system > const > eq_ref > ref > range > index > ALL。
    优化目标:至少要达到range级别,最好能达到ref。如果出现index或ALL,意味着全索引扫描或全表扫描,必须进行优化。
  • key(实际使用的索引):检查查询是否命中了预期的索引。如果key列为NULL,说明没有使用索引,这是导致全表扫描的常见原因。
  • key_len(索引长度):表示MySQL在索引里使用的字节数。
  • rows(预估扫描行数):表示MySQL为了找到目标行,预估需要读取多少行数据。这个值应该尽可能小。如果数值巨大,说明查询效率低下。
  • Extra(额外信息):这里包含了非常多的重要信息。
    Using filesort:MySQL无法利用索引完成排序,而是使用了外部排序。这在ORDER BY子句未用索引时出现,通常需要优化。
    Using temporary:MySQL为了执行查询,需要创建临时表。常见于GROUP BY和ORDER BY子句涉及不同列的查询,对性能影响很大,需要优化。
    Using where:这不一定代表坏事情。它表示在存储引擎检索行后,MySQL服务器层又进行了过滤。但如果type是ALL且Using where,则确实是全表扫描后过滤,性能极差。
    Using index:这是一个好的信号,表示查询使用了覆盖索引,所有需要的数据都能在索引中找到,避免了“回表”操作,极大地提升了性能。
2、MySQL超大分页怎么处理? ⭐⭐⭐⭐

“MySQL超大分页,比如 LIMIT 1000000, 20,性能瓶颈在于 OFFSET 偏移量过大。MySQL需要先读取 1000000 + 20 条记录,然后丢弃前100万条,这个过程非常耗时。

我的解决方案是利用覆盖索引和子查询进行优化,核心思路是把基于位置的偏移,转变为基于ID的条件过滤。

标准优化写法如下:

SELECT * FROM your_table 
WHERE id >= (
    SELECT id FROM your_table 
    ORDER BY id -- 确保与主查询排序一致
    LIMIT 1000000, 1
) 
ORDER BY id 
LIMIT 20;

子查询 SELECT id FROM …:它只查询 id 字段。由于 id 是主键(或建有索引),这个查询成为一个 ‘覆盖索引’查询。MySQL可以完全在索引B+Tree中扫描和定位,而无需访问数据行。虽然它依然要扫描100多万个索引项,但索引体积小,IO速度远快于读取完整数据行。
外层查询 SELECT * … WHERE id >= …:子查询返回了一个起始ID(即第1000001条记录的ID),外层查询直接通过主键 id 进行高效的范围查询。这个操作通常只需要读取少量的完整数据行(比如20条),性能极高。

3、SQL的优化经验有哪些? ⭐⭐⭐⭐⭐
  • 表结构优化
    原则:根据数据范围选择最小适用类型,避免空间浪费。
    字段类型:数值用TINYINT/INT/BIGINT,字符串用CHAR/VARCHAR(定长选CHAR),大文本用TEXT。

  • 索引优化
    高频字段建索引:WHERE、ORDER BY、GROUP BY的字段。
    复合索引覆盖查询:如(a,b)覆盖SELECT a,b。
    避免失效操作:索引字段不做运算、不隐式类型转换。
    前缀索引:长字段(如地址)取前N个字符。

  • SQL语句优化
    拒绝SELECT *:只取必要字段,减少传输和回表。
    JOIN策略:优先INNER JOIN,必须用LEFT/RIGHT JOIN时小表驱动大表。
    聚合优化:UNION ALL替代UNION(无需去重时)。
    分页技巧:避免大偏移量,改用WHERE id > N。

  • 架构扩展
    读写分离:主库写,从库读,分摊压力。
    分库分表:数据量大时,按业务垂直拆分,或按规则(如用户ID哈希)水平拆分。

4.2、索引

1、什么是索引 ⭐⭐⭐⭐⭐

它是一种帮助MySQL高效获取数据的数据结构,主要用来提高数据检索效率,降低数据库的I/O成本。同时,索引列可以对数据进行排序,降低数据排序的成本,也能减少CPU的消耗。

2、索引的底层数据结构了解过吗?B树和B+树的区别是什么呢? ⭐⭐⭐⭐⭐

MySQL的默认存储引擎InnoDB使用的是B+树作为索引的存储结构。选择B+树的原因包括:

  • “首先,相比于二叉树,它的多路平衡特性使得树高很低,大大减少了磁盘 I/O 次数。”
  • “其次,相比于它的前身 B-Tree,它做了两个关键优化:
    一是数据只存放在叶子节点,让非叶子节点更‘瘦’,能承载更多键,进一步降低树高,并且让查询性能更稳定。
    二是叶子节点形成了有序链表,这使得它非常适合数据库频繁进行的范围查询和排序操作。”
  • 对比哈希索引:“而哈希索引虽然等值查询快,但不支持范围查询和排序,所以不适合作为通用的索引数据结构。”
3、什么是聚簇索引、非聚簇索引、回表查询、覆盖索引 ⭐⭐⭐⭐⭐
  • 聚簇索引是指数据与索引放在一起,B+树的叶子节点保存了整行数据,通常只有一个聚簇索引,一般是由主键构成。
  • 非聚簇索引则是数据与索引分开存储,B+树的叶子节点保存的是主键值,可以有多个非聚簇索引,通常我们自定义的索引都是非聚簇索引。
  • 回表查询是指通过二级索引(非聚簇索引)找到对应的主键值,然后再通过主键值查询聚簇索引中对应的整行数据的过程。
  • 覆盖索引是指在SELECT查询中,返回的列全部能在索引中找到,避免了回表查询,提高了性能。使用覆盖索引可以减少对主键索引的查询次数,提高查询效率。
4、索引创建原则有哪些? ⭐⭐⭐⭐
  • 表中的数据量超过10万以上时考虑创建索引。
  • 选择查询频繁的字段作为索引,如查询条件、排序字段或分组字段。
  • 尽量使用复合索引,覆盖SQL的返回值。
  • 如果字段区分度不高,可以将其放在组合索引的后面。
  • 对于内容较长的字段,考虑使用前缀索引。
  • 控制索引数量,因为索引虽然可以提高查询速度,但也会影响插入、更新的速度
5、什么情况下索引会失效? ⭐⭐⭐⭐⭐
  • 没有遵循最左匹配原则。
  • 使用了模糊查询且%号在前面。
  • 在索引字段上进行了运算或类型转换。
  • 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。

4.3、事务与并发

1、事务的特性是什么? ⭐⭐⭐⭐⭐

事务的特性是ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。例如,A向B转账500元,这个操作要么都成功,要么都失败,体现了原子性。转账过程中数据要保持一致,A扣除了500元,B必须增加500元。隔离性体现在A向B转账时,不受其他事务干扰。持久性体现在事务提交后,数据要被持久化存储。

2、 并发事务带来哪些问题? 怎么解决 ⭐⭐⭐⭐⭐

并发事务三大问题:

  • 脏读:读到其他事务未提交的数据(如回滚后变无效)
  • 不可重复读:同事务内,多次读同一数据结果不同(数据被其他事务修改)
  • 幻读:同事务内,相同条件查询出现新数据行(其他事务插入/删除导致)

解决这些问题的方法是使用事务隔离。MySQL支持四种隔离级别:

  • 未提交读(READ UNCOMMITTED):解决不了所有问题。
  • 读已提交(READ COMMITTED):能解决脏读,但不能解决不可重复读和幻读。
  • 可重复读(REPEATABLE READ):能解决脏读和不可重复读,但不能解决幻读,这也是MySQL的默认隔离级别。
  • 串行化(SERIALIZABLE):可以解决所有问题,但性能较低。
3、undo log和redo log的区别是什么? ⭐⭐⭐⭐⭐

redo log记录的是物理日志,即“在某个数据页上做了什么修改”,用于服务崩溃恢复时,重放已提交的事务,保证事务的持久性。
undo log记录的是逻辑日志,即“修改前的那条记录是什么样子”,用事务回滚时,撤销已做的修改
和实现MVCC,提供多版本数据快照,保证事务的原子性 和隔离性

4、事务中的隔离性是如何保证的呢? ⭐⭐⭐⭐⭐

事务的隔离性主要是通过 MVCC(多版本并发控制) 和 锁机制 两者协同工作来保证的。

  • MVCC 是一种‘乐观’的控制方式,核心解决读-写冲突,实现非阻塞读,大大提高并发性。
  • 锁机制 是一种‘悲观’的控制方式,核心解决写-写冲突,确保对数据的修改是串行化的。

首先,我们看MVCC是如何解决读-写冲突的:
它的核心思想是为每一行数据维护多个历史版本。当一个事务执行普通的SELECT操作(快照读)时,它会看到的是一个在它启动时就确定的、一致的快照。这依赖于三个关键部分:

  • 隐藏字段:每行数据都有DB_TRX_ID(最近修改事务ID)和DB_ROLL_PTR(指向旧版本的回滚指针)。
  • Undo Log:数据被修改前的旧版本会存入Undo Log,并通过回滚指针形成一个版本链。
  • Read View(读视图):这是MVCC的规则引擎。事务在第一次查询时会生成一个Read
    View,其中记录了当前所有活跃事务ID列表等关键信息。根据这个视图,事务在读取数据时,会遍历版本链,只读取在它开始时就已经提交的数据版本,从而实现了可重复读,避免了脏读。

然后,我们看锁机制是如何解决写-写冲突的:
MVCC保证了读不阻塞写,但如果有两个事务同时要修改同一行,就必须用锁来保证顺序。
InnoDB使用行级锁,写操作会默认给涉及的行加上排他锁(X锁),确保同一时间只有一个事务能修改某行。
此外,在可重复读(RR) 级别下,为了彻底解决幻读问题,InnoDB还引入了间隙锁和临键锁(行锁+间隙锁)。它们会锁定一个索引范围,防止其他事务在这个范围内插入新的数据,从而消除了幻读的可能。

4.4、其他

1、MySQL主从同步原理是什么? ⭐⭐⭐⭐

MySQL主从复制的核心是二进制日志(Binlog)。步骤如下:

  • 主库在事务提交时记录数据变更到Binlog。
  • 从库读取主库的Binlog并写入中继日志(Relay Log)。
  • 从库重做中继日志中的事件,反映到自己的数据中。
2、什么是分库分表 ⭐⭐⭐⭐

目的:解决单库/表数据量大、性能瓶颈、高并发压力。
分类:垂直拆分(按结构)、水平拆分(按数据分布)。

  • 垂直分库: 拆分方式:按业务模块划分库(如用户库、订单库)。
    优点:业务解耦、降低单库压力。

  • 水平分库:拆分方式:同一表数据按规则分到多库(如用户ID哈希取模)。
    优点:负载均衡、扩展性强。

  • 垂直分表:拆分方式:按字段拆分表(如常用字段+冷字段/大字段分离)。
    优点:减少单表IO、提升高频查询效率。

  • 水平分表:拆分方式:同一表数据分到多表(如按时间、ID范围)。
    优点:单表数据量小、读写更快。

3、锁机制 ⭐⭐⭐⭐⭐

共享锁(S)与排他锁(X):
共享锁(S锁):读锁,事务读取数据时加锁,其他事务可加S锁但不能加X锁。
排他锁(X锁):写锁,事务修改数据时加锁,其他事务不能加任何锁。
规则:S锁与S锁兼容,S锁与X锁互斥,X锁与X锁互斥。

行锁 vs 表锁:
行锁:InnoDB默认,锁定某一行(基于索引),并发高,但死锁风险高。
表锁:MyISAM默认,锁定整张表,并发低,但无死锁。

间隙锁:
锁定索引记录的间隙(如id=5和id=10之间),防止其他事务插入数据,解决幻读。
仅在**可重复读(RR)**隔离级别下生效。

临键锁
行锁+间隙锁

4、InnoDB vs MyISAM ⭐⭐⭐⭐
对比项 InnoDB MyISAM
事务 ✅ 支持ACID,适合高并发事务场景 ❌ 不支持事务
锁粒度 支持行锁、表锁(默认行锁) 仅表锁,并发性能低
外键 ✅ 支持外键约束 ❌ 不支持外键
崩溃恢复 ✅ 通过redo log实现崩溃后数据自动恢复 ❌ 无崩溃恢复机制,易数据损坏
索引结构 🌳 聚簇索引(数据与主键绑定) 🌲 非聚簇索引(数据与索引分离)
适用场景 读写频繁、需要事务(如订单、支付) 读多写少(如日志表、配置表)
5、数据库三大范式 ⭐⭐
  • 第一范式(1NF) 每一列都是不可再分的原子值,确保表中每个字段都是单一值。例如,将“课程”字段中的“数学, 英语”拆分为两行。
  • 第二范式(2NF) 所有信息必须完全依赖主键,不能只依赖主键的一部分
  • 第三范式(3NF)所有信息必须直接依赖主键,不能间接依赖(A→B→C,则A→C是传递依赖)。

五、SSM+SpringBoot

5.1、Spring

1、单例 Bean 在多线程环境下是否线程安全? ⭐⭐⭐⭐

不是线程安全的。当多用户同时请求一个服务时,容器会给每个请求分配一个线程,这些线程会并发执行业务逻辑。如果处理逻辑中包含对单例状态的修改,比如修改单例的成员属性,就必须考虑线程同步问题。Spring框架本身并不对单例bean进行线程安全封装,线程安全和并发问题需要开发者自行处理。
通常在项目中使用的Spring bean是不可变状态(如Service类和DAO类),因此在某种程度上可以说Spring的单例bean是线程安全的。如果bean有多种状态,就需要自行保证线程安全。最简单的解决办法是将单例bean的作用域由“singleton”变更为“prototype”。

2、请阐述 AOP的概念、原理及应用场景 ⭐⭐⭐⭐⭐
  • AOP,即面向切面编程,在Spring中用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑抽取出来,实现公共模块复用,降低耦合。其核心原理是通过动态代理对目标方法进行拦截。常见的应用场景包括公共日志保存事务处理
  • 使用AOP来记录系统操作日志。定义切入点:使用切点表达式确定哪些方法需要被记录日志。编写环绕通知:在这些方法执行前后,通过环绕通知获取请求方法的相关参数,如类信息、方法信息、注解、请求方式等。保存日志:将获取到的参数保存到数据库中。
  • Spring实现事务的本质是利用AOP完成的,通过@Transactional注解。它对方法前后进行拦截,在执行方法前开启事务,在执行完目标方法后根据执行情况提交或回滚事务。
3、详细解释 IOC的概念、实现方式 ⭐⭐⭐⭐⭐
  • 概念:将对象的创建、依赖管理从程序代码“反转”到容器(如Spring框架)完成。核心是解耦——对象不再直接new,而是由容器统一管理并注入依赖。
    示例:A类依赖B类,传统方式需在A中new B();IOC模式下,容器自动创建B并注入A。

依赖注入三种方法(DI):
构造器注入:通过构造函数参数注入。

 public class UserService {
    private UserDao userDao;
    // 构造器注入UserDao
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

Setter注入:通过setXxx()方法注入依赖。

public class UserService {
    private UserDao userDao;
    // setter方法注入UserDao
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

注解注入:@Autowired自动装配(常用)。

  public class UserService {
    // @Autowired注解自动注入UserDao
    @Autowired
    private UserDao userDao;
    // 或使用@Resource(JDK标准注解)
    @Resource
    private UserDao userDao;
}
4、请列举 Spring 事务失效的常见场景,并如何解决 ⭐⭐⭐⭐⭐

Spring事务的管理是通过AOP动态代理实现的。当你调用一个@Transactional方法时,实际上是在调用一个代理对象的方法。这个代理对象负责在方法执行前开启事务、在方法执行后提交或回滚事务。
如果你的方法调用绕开了这个代理对象,那么事务注解自然就失效了。 这是理解大多数失效场景的根本。

  • 访问权限问题:@Transactional注解在了非public方法上。
  • 自调用问题:同一个类内部的方法调用,绕过了代理。
  • 异常处理问题:要么捕获异常后没有再次抛出,要么抛出的异常类型不是默认回滚的类型(如受检异常)。
  • 底层支持问题:数据库引擎本身不支持事务(如MySQL的MyISAM)。
  • 传播行为配置:配置了不开启事务的传播行为。
5、描述 Spring 中 Bean 的生命周期 ⭐⭐⭐⭐⭐
阶段 关键步骤与扩展点 说明
1. 元数据加载 加载 Bean 定义 (BeanDefinition) 容器读取配置(XML/注解/JavaConfig),解析成 BeanDefinition 对象(Bean 的“蓝图”)。
2. 实例化 创建 Bean 实例 根据 BeanDefinition,通过构造函数反射工厂方法创建对象(此时对象是“空壳”,属性未设置)。
3. 依赖注入 填充属性 (populateBean) 核心步骤! 容器自动注入:
- @Autowired/@Value 注解的字段/方法
- XML/JavaConfig 定义的属性值
4. Aware 注入 处理 Aware 接口 回调接口! 如果 Bean 实现了:
- BeanNameAware: 注入 Bean 的 ID
- BeanFactoryAware: 注入 BeanFactory
- ApplicationContextAware: 注入 ApplicationContext
5. 初始化前 BeanPostProcessor.postProcessBeforeInitialization()
执行 @PostConstruct
关键扩展点!
- 所有 BeanPostProcessorbefore 方法执行
- @PostConstruct 注解的方法在此执行
6. 初始化 执行初始化方法 按顺序执行:
1. InitializingBean.afterPropertiesSet() 接口方法
2. XML/@Bean 中定义的 init-method
7. 初始化后 BeanPostProcessor.postProcessAfterInitialization() 最重要扩展点!
- 所有 BeanPostProcessorafter 方法执行
- AOP 动态代理在此阶段生成! (返回的可能是代理对象)
8. 使用中 Bean 就绪 Bean 完全初始化,存在于单例池(Singleton Pool)中,供应用程序使用。
9. 销毁 销毁 Bean 容器关闭时触发,顺序:
1. @PreDestroy 注解的方法
2. DisposableBean.destroy() 接口方法
3. XML/@Bean 中定义的 destroy-method

流程:定义 -> 实例化 -> 注入 -> Aware -> (Before -> 初始化 -> After) -> 使用 -> 销毁

6、什么是 Spring 的循环依赖?Spring 是如何解决循环依赖问题的? ⭐⭐⭐⭐⭐

循环依赖发生在两个或两个以上的bean互相持有对方,形成闭环。Spring框架允许循环依赖存在,并通过三级缓存解决大部分循环依赖问题:

  • 一级缓存:单例池,缓存已完成初始化的bean对象。

  • 二级缓存:存储已实例化但未初始化的Bean。

  • 三级缓存:缓存ObjectFactory,用于创建bean对象。

解决循环依赖的流程如下:

  1. 实例化A对象,并创建ObjectFactory存入三级缓存。
  2. A在初始化时需要B对象,开始B的创建逻辑。
  3. B实例化完成,也创建ObjectFactory存入三级缓存。
  4. B需要注入A,通过三级缓存获取ObjectFactory生成A对象,存入二级缓存。
  5. B通过二级缓存获得A对象后,B创建成功,存入一级缓存。
  6. A对象初始化时,由于B已创建完成,可以直接注入B,A创建成功存入一级缓存。
  7. 清除二级缓存中的临时对象A。

需要三级缓存的原因:
二级缓存看似足够,但无法处理AOP代理场景。三级缓存通过ObjectFactory延迟决定返回原始对象还是代理对象,确保依赖注入的一致性。

7、Bean的作用域有哪些 ⭐⭐⭐
  • 基础作用域(通用)
    🎯 Singleton(单例) 关键字:容器级唯一 | 无状态工具类 记忆点:Spring 的默认选项,类似 Java 的静态类
    🌀Prototype(原型) 关键字:每次请求新对象 | 有状态会话类 记忆点:类似 new 操作符,适合需要隔离状态的场景
  • Web 核心作用域
    📨 Request(请求级) 关键字:HTTP 请求周期 | 表单处理/请求参数 记忆点:每个请求独立(如购物车临时数据)
    🪪Session(会话级) 关键字:用户会话周期 | 登录凭证/个性化配置 记忆点:浏览器标签页级数据隔离
  • Web 扩展作用域
    🌐 Application(应用级) 关键字:ServletContext 生命周期 | 全局缓存/共享配置 记忆点:相当于 Web
    版的 Singleton,但绑定 Web 容器
    🔌 WebSocket(长连接级) 关键字:WebSocket 会话周期 | 实时通信状态管理 记忆点:聊天室消息会话这类长连接场景
8、Spring事务传播机制 ⭐⭐⭐⭐⭐

Spring 的事务传播机制定义了事务方法在调用其他事务方法时事务的传播行为,共有 7 种:

  • REQUIRED(默认),如果当前存在事务则加入,否则创建新事务,适用于大多数业务方法;
  • REQUIRES_NEW,无论当前是否有事务都创建新事务并挂起当前事务,适用于需要独立事务的方法(如日志记录);
  • SUPPORTS,有事务则加入,无事务则以非事务方式执行,适用于方法可在事务或非事务环境下执行;
  • NOT_SUPPORTED,以非事务方式执行并挂起当前事务,适用于方法不需要事务支持(如查询操作);
  • MANDATORY,有事务则加入,无事务则抛出异常,适用于方法必须在事务中执行;
  • NEVER,以非事务方式执行,有事务则抛出异常,适用于方法不能在事务中执行;
  • NESTED,有事务则在嵌套事务中执行,无事务则创建新事务,适用于部分回滚的场景(如批量处理)。
9、 Spring常见的注解有哪些? ⭐⭐⭐
注解 说明
@Component、@Controller、@Service、@Repository 使用在类上用于实例化Bean
@Autowired 使用在字段上用于根据类型依赖注入
@Qualifier 结合@Autowired一起使用用于根据名称进行依赖注入
@Scope 标注Bean的作用范围
@Configuration 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解
@ComponentScan 用于指定Spring在初始化容器时要扫描的包
@Bean 使用在方法上,标注将该方法的返回值存储到Spring容器中
@Import 使用@Import导入的类会被Spring加载到IOC容器中
@Aspect、@Before、@After、@Around、@Pointcut 用于切面编程(AOP)

5.2、SpringMVC

1、简述 SpringMVC 的完整执行流程 ⭐⭐⭐⭐

SpringMVC的执行流程包括以下步骤:

  • 用户发送请求到前端控制器DispatcherServlet。
  • DispatcherServlet调用HandlerMapping(处理器映射器)找到具体处理器(Controller)。
  • HandlerMapping返回处理器对象及拦截器(如果有)给DispatcherServlet。
  • DispatcherServlet调用HandlerAdapter(处理器适配器)。
  • HandlerAdapter适配并调用具体处理器(Controller)。
  • Controller执行相应的业务逻辑并返回ModelAndView对象。
  • HandlerAdapter将ModelAndView返回给DispatcherServlet。
  • DispatcherServlet传给ViewResolver(视图解析器)进行视图解析。
  • ViewResolver返回具体视图给DispatcherServlet。
  • DispatcherServlet渲染视图并响应用户。
2、SpringMVC常见的注解有哪些? ⭐⭐⭐
注解 说明
@RequestMapping 用于映射请求路径,可以定义在类上和方法上。用于类上,则表示类中的所有的方法都是以该地址作为父路径
@RequestBody 注解实现接收http请求的json数据,将json转换为java对象
@RequestParam 指定请求参数的名称
@PathVariable 从请求路径下中获取请求参数(/user/{id}),传递给方法的形式参数
@ResponseBody 注解实现将controller方法返回对象转化为json对象响应给客户端
@RequestHeader 获取指定的请求头数据
@RestController @Controller + @ResponseBody

5.3、SpringBoot

1、请阐述 Spring Boot 自动配置的原理 ⭐⭐⭐⭐⭐
  • 在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解对三个注解进行了封装,分别是:@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan
  • 其中@EnableAutoConfiguration是实现自动化配置的核心注解。@EnableAutoConfiguration通过@Import注解导入AutoConfigurationImportSelector类。这个类会读取项目及其引用的 Jar 包的 classpath 路径下META - INF/spring.factories文件。该文件里定义了一系列自动配置类的全类名
  • 从spring.factories文件中读取到的自动配置类并非都会被加载到 Spring 容器,而是要依据条件注解来判断。常见的条件注解有@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。
2、Springboot常见注解有哪些? ⭐⭐⭐
注解 说明
@SpringBootConfiguration 组合了@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration 打开自动配置的功能,也可以关闭某个自动配置选项

5.4、Mybatis

1、mybatis的#{} 和 ${} 的区别是什么? ⭐⭐⭐⭐
  • SQL 注入风险
    #{}:使用预编译语句(PreparedStatement)的参数占位符(?),能有效防止 SQL 注入。
    ${}:直接进行字符串替换,相当于拼接 SQL 字符串,存在 SQL 注入风险。

  • 适用场景
    #{}:适用于所有需要传入参数值的场景(如 WHERE 条件、INSERT 值等)。
    ${}:主要用于动态传入表名、列名或排序字段等 SQL 结构部分(需确保参数来源安全)。

  • 参数处理
    #{}:MyBatis 会自动处理参数类型,如字符串会自动添加引号。
    ${}:直接替换为原始值,需手动处理引号等格式。

2、 Mybatis是否支持延迟加载以及底层原理 ⭐⭐⭐

MyBatis支持延迟加载,即在需要用到数据时才加载。可以通过配置文件中的lazyLoadingEnabled配置启用或禁用延迟加载。
MyBatis 的延迟加载是通过 动态代理 实现的。具体步骤如下:

  • 创建代理对象:当查询主对象时,MyBatis 不会立即加载关联对象,而是为关联对象创建一个代理对象(如 Proxy)。这个代理对象看起来和实际对象一样,但实际上并未加载数据。

  • 触发加载:当程序首次访问代理对象的某个方法(如 getXXX())时,代理对象会拦截这次调用。在拦截器中,MyBatis 执行关联查询(即延迟加载),并将结果设置到代理对象中。

  • 返回结果:延迟加载完成后,代理对象将加载的数据返回给调用方。

3、讲讲Mybatis的一级、二级缓存 ⭐⭐⭐⭐
  • 一级缓存(本地缓存)是 SqlSession 级别 的缓存,默认开启。同一个 SqlSession 中执行的查询结果会被缓存。当 SqlSession被关闭或清空时,一级缓存会被清除。执行 insert、update、delete 操作时,一级缓存也会被清空。
  • 二级缓存(全局缓存)是 Mapper 级别的缓存,需要手动开启。一级缓存未命中时,查找二级缓存。多个 SqlSession共享同一个二级缓存。二级缓存的生命周期与整个应用一致,只有当应用关闭或显式清空缓存时,二级缓存才会被清除。执行insert、update、delete 操作时,二级缓存会被清空。

六、JUC

6.1丶线程基础知识

1、进程和线程的区别 ⭐⭐⭐⭐

定义:

  • 进程是操作系统资源分配的基本单位,是正在运行的程序的实例。
  • 线程是进程内的执行单元,是CPU调度的基本单位。

资源分配:

  • 进程拥有独立的内存空间和资源,进程间资源隔离。
  • 线程共享进程的内存空间和资源,线程间通信更方便。

上下文切换:

  • 进程切换开销大,需要保存和恢复整个进程的状态。
  • 线程切换开销小,只需保存和恢复线程的执行状态。

独立性:

  • 进程间相互独立,一个进程崩溃不会影响其他进程。
  • 线程共享进程资源,一个线程崩溃可能导致整个进程崩溃。
2、并行和并发有什么区别? ⭐⭐⭐⭐
  • 并发是同一时间段内处理多件事情(如多线程轮流使用CPU),适合I/O密集型任务。

  • 并行是同一时刻执行多件事情(如多核CPU同时运行多个线程),适合CPU密集型任务。

3、 创建线程的四种方式 ⭐⭐⭐⭐
  • 继承Thread类: 重写run()方法,直接调用start()启动线程。 缺点:单继承限制,扩展性差。

  • 实现Runnable接口:实现run()方法,通过Thread类启动线程。 优点:避免单继承限制,适合资源共享。

  • 实现Callable接口:实现call()方法,可以返回结果和抛出异常,通常与FutureTask或线程池结合使用。

  • 线程池创建线程:通过线程池管理线程,避免频繁创建和销毁线程的开销。 优点:资源复用、控制并发数、提供定时任务等功能。

  • 总结:项目中通常使用线程池创建线程,因为线程池可以更好地管理线程资源,避免资源浪费和性能问题。

4、runnable 和 callable 有什么区别 ⭐⭐⭐⭐

返回值:

  • Runnable 的 run() 方法没有返回值。
  • Callable 的 call() 方法有返回值,返回类型是泛型。

异常处理:

  • Runnable 的 run() 方法不能抛出受检异常,只能在内部处理。
  • Callable 的 call() 方法可以抛出受检异常。

使用场景:

  • Runnable 适合简单的异步任务,不需要返回值。
  • Callable 适合需要返回结果或抛出异常的任务。

执行方式:

  • Runnable 通过 Thread 或线程池执行。
  • Callable 通常与 Future 或线程池结合,通过 FutureTask.get() 获取结果(此方法会阻塞主线程)。
5、 线程的 run()和 start()有什么区别?⭐⭐⭐⭐

功能:

  • start() 用于启动新线程,JVM 会自动调用 run() 方法执行任务。
  • run() 封装了线程要执行的逻辑代码,直接调用不会启动新线程。

调用次数:

  • start() 只能调用一次,多次调用会抛出异常。
  • run() 可以调用多次。

执行方式:

  • start() 是异步执行,不会阻塞主线程。
  • run() 是同步执行,会阻塞当前线程。

实际应用:

  • start() 用于启动新线程,执行异步任务(如并发处理、耗时操作)。
  • run() 通常用于测试或直接执行任务逻辑,不启动新线程。
6、线程包括哪些状态,状态之间是如何变化的 ⭐⭐⭐⭐⭐

线程的六种状态:
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、有时限等待(TIMED_WAITING)、终结(TERMINATED)。

状态切换:

  • NEW → RUNNABLE:调用 start() 方法,线程进入可运行状态。
  • RUNNABLE → TERMINATED:线程代码执行完毕,进入终止状态。
  • RUNNABLE → BLOCKED:线程获取锁失败,进入阻塞状态;锁释放后恢复为可运行状态。
  • RUNNABLE → WAITING:调用 wait()、join() 方法,线程进入等待状态;被唤醒后恢复为可运行状态。
  • RUNNABLE → TIMED_WAITING:调用 sleep(long)、wait(long)方法,线程进入有时限等待状态;超时后恢复为可运行状态。

实际应用:

  • 多线程竞争锁时会发生 RUNNABLE → BLOCKED 切换。
  • 线程间协作或等待资源时会发生 RUNNABLE → WAITING/TIMED_WAITING 切换。
7、新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?⭐⭐⭐

核心原理是利用join()方法的特性:调用某个线程的join()方法,会让当前线程阻塞,直到调用的线程执行完毕。

可以用线程类的join()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
比如说:使用join方法,T3调用T2的join,T2调用T1join,这样就能确保T1就会先完成而T3最后完成

8、notify()和 notifyAll()有什么区别? ⭐⭐⭐⭐
  • notifyAll:唤醒所有wait的线程

  • notify:只随机唤醒一个 wait 线程

9、在 java 中 wait 和 sleep 方法的不同? ⭐⭐⭐⭐⭐

共同点:wait() 和 sleep() 都会让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。

方法归属:

  • wait() 是 Object 的成员方法,每个对象都有。
  • sleep() 是 Thread 的静态方法。

醒来时机:

  • wait() 可以在指定时间后自动醒来,也可以被 notify() 或 notifyAll() 唤醒。
  • sleep() 只能超时自动醒来或被中断唤醒。

锁特性:

  • wait() 调用前必须先获取对象锁,执行后会释放锁。
  • sleep() 调用前无需获取锁,执行期间也不会释放锁。

使用场景:

  • wait() 用于线程间通信,通常与 synchronized 配合使用。
  • sleep() 用于让线程暂停执行一段时间。
10、如何停止一个正在运行的线程? ⭐⭐⭐

有三种方式可以停止线程

  • 使用退出标志:定义一个布尔类型的标志位,线程在执行过程中会不断检查该标志位的状态。若标志位被设置为特定值,线程就会停止执行。这是一种较为安全且推荐使用的方法。
  • 使用stop方法:强行终止,可能会导致线程持有的锁被突然释放(不推荐,方法已作废)
  • 使用interrupt方法:线程在运行过程中可以通过检查自身的中断状态来决定是否停止执行。

6.2丶线程并发安全

1、 讲一下synchronized关键字的底层原理? ⭐⭐⭐⭐⭐

synchronized 是 Java 中用于实现线程同步的关键字,它的底层原理是基于 JVM 的 Monitor(监视器锁)实现的,每个 Java 对象都可以关联一个 Monitor。synchronized 通过操作对象头中的锁标志位来获取和释放锁,若锁标志位显示对象处于无锁状态,线程会尝试修改锁标志位以获取锁,同时关联对应的 Monitor,之后进入同步代码块执行。

Monitor 内部维护了三个关键部分:

  • Owner:当前持有锁的线程。
  • EntryList:等待获取锁的线程队列(阻塞状态)。
  • WaitSet:调用了 wait() 方法而进入等待状态的线程队列。

当线程尝试获取锁时,如果锁已被占用,线程会进入 EntryList 等待;当锁释放时,JVM 会从 EntryList 中唤醒线程竞争锁。synchronized 是一种悲观锁,假设并发环境下会发生冲突,因此每次访问共享资源时都会加锁。

在 JDK 1.6 之后,synchronized 引入了锁升级机制(无锁 → 偏向锁 → 轻量级锁 → 重量级锁),以优化性能。不过,由于 synchronized 依赖于 JVM 级别的 Monitor,它的性能相对较低,尤其是在高并发场景下。

2、了解synchronized锁升级吗? ⭐⭐⭐⭐
  • 无锁
    对象刚被创建时,Mark Word 的锁标志位为 01,偏向锁标志位为 0,表示无锁状态。

  • 偏向锁
    适用场景:单线程反复访问同步代码块(无竞争)。
    实现原理:对象头记录第一个获取锁的线程ID。后续该线程进入同步代码块时,只需检查线程ID是否一致,无需任何锁操作(直接通行)。
    优点:无竞争时几乎零开销(仅内存读取)。
    缺点:一旦有其他线程竞争,立即升级为轻量级锁。

  • 轻量级锁
    适用场景:低竞争(如线程交替执行,无并发冲突)。
    实现原理:使用CAS尝试修改对象头的锁标记。成功:获取锁;失败:自旋(循环等待)一小段时间。若自旋失败,升级为重量级锁。
    优点:避免线程阻塞(用户态解决竞争),开销小于重量级锁。
    缺点:自旋消耗CPU,高竞争下性能下降。

  • 重量级锁
    适用场景:高竞争(多线程同时抢锁)。
    实现原理:基于操作系统底层的互斥量(mutex)实现。未抢到锁的线程会被挂起,进入等待队列,由操作系统调度唤醒。
    优点:严格保证线程安全,适合高并发场景。
    缺点:涉及用户态到内核态的切换,性能开销最大。

  • 锁升级流程
    无锁 → 偏向锁:首次线程访问同步块时,对象头记录线程ID。
    偏向锁 → 轻量级锁:其他线程尝试竞争时,撤销偏向锁,改用CAS自旋。
    轻量级锁 → 重量级锁:自旋失败(如自旋次数超过阈值,默认10次)或竞争加剧时升级。
    ⚠️ 注意:锁升级是单向的(不可降级),由JVM自动完成。

3、谈谈 JMM(Java 内存模型) ⭐⭐⭐⭐

Java内存模型是Java虚拟机规范中定义的一种非常重要的内存模型。它的主要作用是描述Java程序中线程共享变量的访问规则,以及这些变量在JVM中是如何被存储和读取的,涉及到一些底层的细节。

  • 这个模型有几个核心的特点。首先,所有的共享变量,包括实例变量和类变量,都被存储在主内存中,也就是计算机的RAM。需要注意的是,局部变量并不包含在内,因为它们是线程私有的,所以不存在竞争问题。

  • 其次,每个线程都有自己的工作内存,这里保留了线程所使用的变量的工作副本。这意味着,线程对变量的所有操作,无论是读还是写,都必须在自己的工作内存中完成,而不能直接读写主内存中的变量。

  • 最后,不同线程之间不能直接访问对方工作内存中的变量。如果线程间需要传递变量的值,那么这个过程必须通过主内存来完成。

4、 什么是CAS? ⭐⭐⭐⭐

CAS(Compare And Swap) 是一种乐观锁的实现方式。该操作涉及三个操作数:内存位置(V)、预期原值(A)和新值(B)。其核心思想是,仅当内存位置 V 中的值与预期原值 A 相同时,才会将该内存位置的值更新为新值 B;若不同,则不进行更新操作。

应用场景:

  • AQS 框架:用于实现线程的排队和锁的获取与释放。
  • AtomicXXX 类:是Java提供的原子操作工具类,如 AtomicInteger、AtomicLong 等
  • 集合框架:ConcurrentHashMap、CopyOnWriteArrayList等使用CAS优化线程安全操作。
  • 轻量级锁(自旋锁):Java的轻量级锁在竞争时通过CAS尝试修改对象头中的锁标记。

优点:

  • 无锁并发:CAS 是一种无锁算法,避免了传统锁机制带来的线程阻塞和上下文切换开销,在高并发场景下能显著提升性能。
  • 原子性:CAS 操作是原子性的,这意味着在多线程环境中,该操作要么全部完成,要么完全不执行,不会出现部分更新的情况,保证了数据的一致性。

缺点:

  • ABA 问题:若一个值从 A 变为 B,再从 B 变回 A,CAS 操作会认为该值没有发生变化,从而继续执行更新操作。可以通过版本号或时间戳解决。
  • 自旋开销:当 CAS 操作失败时,线程通常会进行自旋重试。若长时间重试都无法成功,会消耗大量的 CPU 资源。
5、乐观锁和悲观锁 ⭐⭐⭐⭐

乐观锁:

  • 思想:假设在大多数情况下不会发生冲突,允许多个线程同时访问共享资源,只有在更新数据时才会检查是否有冲突。
  • 实现方式:通常通过版本号或时间戳来实现。
  • 优点:在高并发、低冲突的场景下,性能较好。
  • 缺点:在冲突较多的情况下,可能会导致频繁的重试。

悲观锁:

  • 思想:假设在大多数情况下会发生冲突,因此在访问共享资源时,先获取锁,确保其他线程无法修改数据,直到当前线程释放锁。
  • 实现方式:通常通过 synchronized 关键字或 ReentrantLock 来实现。
  • 优点:在冲突较多的场景下,可以有效地保证数据的一致性。
  • 缺点:性能开销较大,可能会导致线程阻塞。
6、谈谈你对 volatile 的理解⭐⭐⭐⭐

volatile 是 Java 中的一个关键字,用于修饰类的成员变量或静态成员变量,主要解决多线程环境下的可见性和有序性问题。
功能:

  • 可见性:volatile修饰的变量在被修改后,会立即写回主内存,并且其他线程在读取该变量时,会直接从主内存中获取最新值,确保变量的修改对所有线程立即可见。
  • 禁止指令重排序:Java 编译器和处理器为了提高性能,可能会对指令进行重排序,只要保证程序的最终执行结果和顺序执行的结果一致即可。但在多线程环境下,指令重排序可能会导致程序出现错误。volatile 修饰的变量会禁止编译器和处理器对其进行指令重排序,保证代码执行的有序性。

底层实现:
通过插入内存屏障来实现可见性和禁止指令重排序。

7、什么是AQS? ⭐⭐⭐⭐

AQS (AbstractQueuedSynchronizer) 是 Java 并发包中的一个核心框架,它为构建锁和其他同步器提供了基础实现。许多 Java 并发工具如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。

核心思想:AQS 的核心是基于一个 volatile int 类型的同步状态变量(state)来表示同步状态,通过 CAS 操作来修改这个状态。当线程请求获取同步资源时,如果当前状态满足条件,则获取成功;否则,线程会被包装成一个节点(Node)并加入到一个双向队列(CLH 队列)中等待。当持有同步资源的线程释放资源时,会从队列中唤醒等待的线程,使其有机会获取同步资源。

应用场景:
AQS 定义了需要子类实现的抽象方法,具体同步器实现这些方法来定义自己的同步逻辑

  • 实现阻塞式锁ReentrantLock 就是基于 AQS 实现的独占锁,通过 AQS 来管理锁的获取和释放,实现线程的同步。
  • 实现信号量Semaphore 利用 AQS 来控制同时访问某个资源的线程数量,通过 acquire 和 release 方法来获取和释放信号量。
  • 实现倒计时锁CountDownLatch 通过 AQS 实现了一种线程等待机制,让一组线程等待其他线程完成一系列操作后再继续执行。
8、ReentrantLock的实现原理 ⭐⭐⭐

ReentrantLock 是 Java 并发包中的一个可重入锁实现,属于 API 层面的锁,与 synchronized 一样,都是悲观锁。

底层实现:

AQS:状态变量 state,初始值为 0,表示锁未被持有。被线程持有时,state 递增(支持重入),释放时递减,减为 0 时锁被完全释放。
FIFO 等待队列:未获取到锁的线程会被封装成 Node 节点进入队列阻塞,等待唤醒。

核心特性:

  • 可重入性:线程可以重复获取同一把锁,重入次数通过 state变量记录。每次获取锁时,重入次数增加;每次释放锁时,重入次数减少。重入次数减到 0 时,锁完全释放。
  • 核心方法:lock() 获取锁,unlock() 释放锁,tryLock() 尝试获取锁,lockInterruptibly()可中断获取锁。

公平锁与非公平锁:

  • 非公平锁:默认模式,允许“插队”,提高吞吐量。
  • 公平锁:按照线程等待顺序分配锁,避免线程饥饿。
9、 synchronized和ReentrantLock有什么区别 ? ⭐⭐⭐⭐⭐

好的,以下是优化后的对比表格:

ReentrantLock 与 synchronized 的核心差异

特性 ReentrantLock synchronized
公平性 支持公平/非公平锁(通过构造函数选择) 仅支持非公平锁
可中断性 支持(lockInterruptibly() 不支持
尝试锁 支持(tryLock() 不支持
锁超时 支持(tryLock(timeout, unit) 不支持
条件变量 支持多个 Condition 对象(精准唤醒) 单个 wait()/notify() 队列
性能 高并发下表现更优 低竞争下 JVM 会优化(锁升级)

关键差异解释

  1. 公平锁ReentrantLock 可通过 new ReentrantLock(true) 创建公平锁,保证线程按请求顺序获取锁,避免饥饿;synchronized 无法实现公平性。

  2. 可中断锁lockInterruptibly() 允许线程在等待锁时响应中断,防止死锁。

  3. 尝试锁tryLock() 支持无阻塞获取锁,或通过超时参数避免长时间等待。

  4. 条件变量ReentrantLockCondition 支持多个等待队列(如生产者-消费者模式中的 notFullnotEmpty),而 synchronized 只能有一个等待队列。

  5. 性能

    • 低竞争:两者性能接近,JVM 会对 synchronized 进行锁升级优化。
    • 高竞争ReentrantLock 的 CAS 操作避免线程挂起,性能更优。
10、死锁产生的条件是什么?⭐⭐⭐⭐

死锁产生的条件包括以下四个必要条件:

  • 互斥条件:资源一次只能被一个线程占用。
  • 占有并等待:线程在持有资源的同时,还在等待获取其他资源。
  • 不可抢占:线程已获取的资源不能被其他线程强行抢占。
  • 循环等待:存在一个线程等待的循环链,每个线程都在等待下一个线程占用的资源。

如果 T1 持有 A 锁并等待 B 锁,而 T2 持有 B 锁并等待 A 锁,就会形成死锁。
避免死锁的方法:

  • 破坏占有并等待条件:一次性申请所有资源。
  • 破坏不可抢占条件:允许资源抢占。
  • 破坏循环等待条件:对资源进行排序并按顺序申请。
11、 如何进行死锁诊断?⭐⭐⭐

jps:输出JVM中运行的进程状态信息
jstack:查看java进程内线程的堆栈信息

  • 先通过jps来查看当前java程序运行的进程id,

输入:

 jps

输出:

1234 MainClass
5678 AnotherClass
  • 然后通过jstack来查看这个进程id,就能展示出来死锁的问题

输入:

jstack 1234
  • 并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。
12、 导致并发程序出现问题的根本原因是什么 ⭐⭐⭐

导致并发程序出现问题的根本原因 可以归结为 Java 并发编程的三大核心特性:原子性、可见性 和 有序性。

原子性:

  • 定义:操作不可分割,要么全部执行成功,要么全部不执行。

  • 问题:如 i++ 操作可能导致数据不一致。

  • 解决方案:使用 synchronized、Lock 或 AtomicXXX 类。

可见性:

  • 定义:一个线程对共享变量的修改能够及时被其他线程看到。

  • 问题:线程可能读取到共享变量的旧值。

  • 解决方案:使用 volatile、synchronized 或 Lock。

有序性:

  • 定义:程序执行顺序按照代码的先后顺序执行。

  • 问题:指令重排序可能导致多线程环境下程序行为异常。

  • 解决方案:使用 volatile 或 synchronized 禁止指令重排序。

6.3丶线程池

1、说一下线程池的核心参数(线程池的执行原理知道嘛)⭐⭐⭐⭐⭐

在线程池中一共有7个核心参数:

  • corePoolSize 核心线程数目 - 池中会保留的最多线程数
  • maximumPoolSize 最大线程数目 - 核心线程+救急线程的最大数目
  • keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
  • unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
  • workQueue 队列 - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
  • threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
  • handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
    拒绝策略有4种,当线程数过多以后,第一种是抛异常、第二种是由调用者执行任务、第三是丢弃当前的任务,第四是丢弃最早排队任务。默认是直接抛异常。

执行原理

  • 判断核心线程数:线程池首先会检查当前正在运行的线程数量是否小于核心线程数(corePoolSize)。如果是,则创建一个新的工作线程来执行该任务,即使此时线程池中可能有空闲的线程。
  • 放入任务队列:如果当前正在运行的线程数量已经达到或超过核心线程数,线程池会尝试将任务放入任务队列中。如果任务队列未满,任务会被成功放入队列,等待有空闲的工作线程来执行。
  • 创建新线程:如果任务队列已满,线程池会检查当前正在运行的线程数量是否小于最大线程数(maximumPoolSize)。如果是,则创建一个新的工作线程来执行该任务。
  • 执行拒绝策略:如果当前正在运行的线程数量已经达到最大线程数,并且任务队列也已满,线程池会执行拒绝策略来处理新提交的任务。
2、线程池中有哪些常见的阻塞队列 ⭐⭐⭐⭐

ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列,它们在实现和使用上有一些关键的区别。

  • 首先,ArrayBlockingQueue是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
  • 其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
  • 另外,它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
3、如何确定核心线程数 ⭐⭐⭐

CPU 密集型任务:
核心线程数 = CPU 核心数 + 1,充分利用 CPU 资源,减少空闲。如 8 核 CPU,设核心线程数为 9。
I/O 密集型任务:
理论公式:核心线程数 = CPU 核心数 * [1 + (I/O 等待时间 / CPU 计算时间)]。例如,I/O 等待 5ms,CPU 计算 1ms,4 核 CPU,则核心线程数为 24。
经验法则:常将核心线程数设为 CPU 核心数的 2 倍或更多,如 4 核 CPU,可设 8 - 16,再性能测试定最佳值。
混合型任务:
分析 CPU 与 I/O 占比,依占比大的部分参考对应类型设置,或先按经验值设初始值,经性能测试和监控调整优化。

4、 线程池的种类有哪些 ⭐⭐

在jdk中默认提供了4中方式创建线程池

  • SingleThreadExecutor
    特点:线程池中只有一个线程,
    核心线程数 = 最大线程数 = 1(仅一个工作线程);
    非核心线程存活时间 = 0;
    任务队列:LinkedBlockingQueue(无界队列);
  • FixedThreadPool
    特点:线程池中的线程数量固定。
    核心线程数 = 最大线程数 = 传入的nThreads(线程数固定);
    非核心线程存活时间 = 0(因为核心线程数 = 最大线程数,无意义);
    任务队列:LinkedBlockingQueue(无界队列,默认容量Integer.MAX_VALUE);
  • CachedThreadPool
    特点:缓存线程池,适用短时间内有大量临时任务的场景
    核心线程数 = 0;
    最大线程数 = Integer.MAX_VALUE(理论上无限制);
    非核心线程存活时间 = 60 秒(空闲 60 秒后销毁);
    任务队列:SynchronousQueue(同步队列,无容量,不存储任务,仅做任务传递);
  • ScheduledThreadPool
    特点:主要用于定时任务和周期性任务的执行。
    核心线程数 = 传入的corePoolSize;
    最大线程数 = Integer.MAX_VALUE;
    非核心线程存活时间 = 0(空闲后立即销毁);
    任务队列:DelayedWorkQueue(延迟队列,按任务延迟时间排序);
5、 为什么不建议用Executors创建线程池 ⭐⭐⭐⭐

主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。

6.4丶使用场景

1 、你们项目哪里用到了线程池 ⭐⭐⭐

订单创建:当用户下单后,需要进行一系列操作,如验证用户信息、检查商品库存、计算订单金额等。将这些操作封装成任务提交到线程池,可以并行处理多个订单的创建任务,提高系统的并发处理能力,减少用户等待时间。

2、如何控制某个方法允许并发访问线程的数量? ⭐⭐⭐

在jdk中提供了一个Semaphore类(信号量)
它提供了两个方法,semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了。第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1

3、谈谈你对ThreadLocal的理解 ⭐⭐⭐⭐⭐

两个主要功能:

  • 资源对象的线程隔离:能让每个线程拥有并使用自己独立的资源对象,避免因多个线程争用同一资源而引发线程安全问题。
  • 线程内的资源共享:可在同一个线程的不同代码片段之间共享资源对象。

底层原理
ThreadLocal 内部维护了一个 ThreadLocalMap 类型的成员变量,用于存储资源对象。具体操作如下:

  • set 方法:调用此方法时,会以 ThreadLocal 自身作为 key,要存储的资源对象作为 value,将其存入当前线程的ThreadLocalMap 集合中。
  • get 方法:调用该方法时,会以 ThreadLocal 自身作为 key,在当前线程的 ThreadLocalMap中查找与之关联的资源值。
  • remove 方法:调用此方法时,会以 ThreadLocal 自身作为 key,从当前线程的 ThreadLocalMap中移除与之关联的资源值。

内存溢出问题
ThreadLocalMap 中的 key 被设计为弱引用,在垃圾回收(GC)时,key 会被被动回收。然而,value 是强引用,不会被自动回收。通常,ThreadLocal 会被定义为静态变量(强引用),无法依赖 GC 自动回收。为避免内存溢出,建议在使用完 ThreadLocal 后,主动调用 remove 方法释放 key 和对应的 value。

七、JVM

7.1丶JVM组成

1、JVM由那些部分组成,运行流程是什么? ⭐⭐⭐⭐⭐

在这里插入图片描述

JVM 主要由类加载子系统运行时数据区执行引擎本地方法接口四大部分组成:

  • 类加载子系统:加载 Java 字节码文件(.class文件)到 JVM 中,并生成对应的Class对象,供运行时数据区使用。包括:启动类加载器、扩展类加载器、应用程序类加载器、自定义类加载器
  • 运行时数据区:包括堆(存对象实例和数组)、虚拟机栈(存储方法调用时的局部变量表、操作数栈、动态链接、方法出口等)、本地方法栈(为本地方法提供内存空间)、方法区(存类信息、静态变量、常量池)、程序计数器(记录当前线程正在执行的字节码指令的地址)。
  • 执行引擎:负责执行字节码指令,将字节码转换为特定平台的机器码。包含解释器(逐行解释执行字节码)和JIT编译器(将热点代码编译为本地机器码以提升执行效率)和垃圾回收器GC。
  • 本地方法接口:调用操作系统或C/C++库的功能。

运行流程:

  • 类加载:通过类加载器加载.class文件,经过加载、验证、准备、解析、初始化五个步骤,将类信息存入方法区,并在堆中创建Class对象。
  • 内存分配:JVM划分运行时数据区:堆存储对象实例,栈管理方法调用(线程私有),方法区存类元信息,程序计数器记录执行位置。
  • 执行指令:执行引擎解释或编译(JIT)字节码为机器码,由CPU执行;必要时通过本地方法接口调用系统资源,垃圾回收(GC)自动回收堆内存。
2、什么是程序计数器 ⭐⭐⭐

程序计数器是JVM中线程私有的内存区域,每个线程都有一个独立的程序计数器。

  • 作用:记录当前线程正在执行的字节码指令的地址。
  • 特点:不会发生内存溢出的区域,所以也不会垃圾回收
  • 意义:确保线程切换后能恢复到正确的执行位置。决定字节码的执行顺序。
3、什么是Java堆? ⭐⭐⭐

Java堆是JVM管理的最大一块内存区域,所有线程共享,用于存储对象实例和数组。

组成:

  • 年轻代:包括Eden区和两个Survivor区,新创建的对象首先分配在Eden区,经过多次GC后存活的对象晋升到老年代。使用复制算法垃圾回收
  • 老年代:存放生命周期较长的对象,GC频率较低。使用标记-清除或标记-整理算法垃圾回收

JDK 1.7与1.8的区别:

  • JDK 1.7使用永久代存储类信息、静态变量等,容易导致内存溢出。

  • JDK 1.8移除了永久代,引入元空间,使用本地内存,默认不限制大小,避免了内存溢出问题。

    在这里插入图片描述

4、什么是虚拟机栈 ⭐⭐⭐

虚拟机栈属于线程私有的内存区域。它主要用于管理方法的调用和执行过程,存储方法运行时所需的局部变量、操作数栈、动态链接和方法出口等信息。栈的生命周期与线程一致,线程结束时,对应的栈内存随之释放。

栈帧:

  • 每次方法调用会创建一个栈帧,存储局部变量表、操作数栈、动态链接、方法返回地址等数据。
  • 当前正在执行的方法对应的栈帧称为活动栈帧,每个线程同一时刻只能有一个活动栈帧。

异常:

  • 栈溢出(StackOverflowError):常见于递归调用过深。
  • 栈内存不足(OutOfMemoryError):线程数过多导致栈内存耗尽

栈内存分配

  • 不是越大越好,默认的栈内存通常为1Mb,栈帧过大会导致线程数变少

局部变量线程安全性

  • 基本数据类型的局部变量(如 int, boolean 等)是线程安全的。
    因为每个线程在调用方法时,都会在各自的栈帧中创建独立的变量副本,互不干扰。

  • 对象类型的局部变量(即引用类型)的线程安全性取决于对象的作用域:
    如果对象仅在方法内创建和使用,且未暴露给其他线程,则是线程安全的。
    如果对象被共享给其他线程(例如通过返回值、静态变量或传递给其他线程),则需要额外同步机制来保证线程安全,此时问题在于对象本身的状态管理。

5、堆栈的区别是什么? ⭐⭐⭐⭐⭐
特性
存储内容 局部变量表、操作数栈、动态链接、返回地址 对象实例、数组
线程共享性 线程私有 线程共享
内存管理 自动分配和释放,速度快 由垃圾回收器(GC)管理,速度较慢
内存大小 较小,默认1MB 较大,默认物理内存的1/4
异常 栈溢出:StackOverflowError 内存不足:OutOfMemoryError
6、什么是方法区,什么是运行时常量池 ⭐⭐⭐⭐

方法区是JVM中所有线程共享的内存区域,用于存储类的元数据、运行时常量池、静态变量、即时编译器编译后的代码等。
内容:

  • 类的元数据:类的名称、字段、方法、接口等描述信息。
  • 运行时常量池:类和接口中的常量(如字符串常量、数字常量等)。
  • 静态变量:类中定义的静态变量。
  • 即时编译器编译后的代码:JIT编译后的机器码。

实现:

  • JDK 1.7及之前:方法区由永久代实现,存储在堆内存中。
  • JDK 1.8及之后:方法区由元空间实现,存储在本地内存中,默认不限制大小。

运行时常量池是方法区的一部分,每个类或接口在 JVM 加载时都会创建对应的运行时常量池。它用于存储:

  • 编译期常量:如字符串字面量(“hello”)、基本数据类型常量(100、3.14)。
  • 符号引用:类和接口的全限定名、字段名和描述符、方法名和描述符。JVM在运行时将其解析为直接引用。
  • 动态生成的常量:如String.intern()方法将字符串添加到常量池中。
7、什么是直接内存? ⭐⭐⭐

直接内存是JVM外部的内存区域,由操作系统的本地内存分配,不由JVM管理。
优点:

  • 减少数据拷贝,提升 IO 效率:当 Java 程序进行网络通信、文件读写或与底层系统交互时。直接内存可直接与内核空间交互,避免 “堆内存→直接内存” 的拷贝步骤。
  • 突破 JVM 堆内存限制:堆内存受-Xmx参数限制,而直接内存由操作系统管理,可利用更大的物理内存。例如,处理超大容量数据(如大数据框架 Hadoop 的块存储)时,直接内存能避免堆内存溢出。
  • 降低 GC 压力:堆内存中的对象会触发 GC,而直接内存的回收由 JVM 通过Unsafe类或DirectByteBuffer的cleaner机制手动控制,可减少 GC 对应用程序的暂停影响

应用场景:

  • NIO 框架(如 Netty)、文件读写(MappedByteBuffer)、图像处理、数据库连接池(如 MySQL 的 JDBC 驱动使用直接内存缓存结果集)等。

直接内存通过牺牲部分易用性(需手动管理)换取性能和内存容量的优势,是 Java 与底层系统高效交互的重要桥梁。

7.2丶类加载器

1、什么是类加载器,类加载器有哪些⭐⭐⭐

类加载器是JVM的核心组件,负责在程序运行时动态加载类文件(.class)
分类:

  • 启动类加载器:加载Java 核心类库,如java.lang.*、java.util.*等。
  • 扩展类加载器:加载扩展类库(JAVA_HOME/jre/lib/ext)。
  • 应用类加载器:加载用户类路径(ClassPath)下的类。
  • 自定义类加载器:用户继承ClassLoader实现自定义加载规则。
2、什么是双亲委派模型,为什么采用双亲委派机制?⭐⭐⭐⭐⭐

双亲委派模型是JVM类加载器的一种工作机制,它定义了类加载器在加载类时的层次关系和协作方式。

核心机制:

  • 当一个类加载器收到类加载请求时,首先委托父类加载器加载。
  • 如果父类加载器无法完成加载任务,子类加载器才会尝试加载。

加载顺序:

  • 启动类加载器 → 扩展类加载器 → 应用类加载器 → 自定义类加载器。

优点:

  • 避免重复加载:父加载器已加载的类,子加载器不会再次加载,保证类的全局唯一性。
  • 安全性:防止用户伪造同名类篡改核心功能
  • 隔离性:不同加载器加载的类默认处于不同命名空间,实现类隔离
3、类装载的执行过程? ⭐⭐⭐⭐

目标:将 .class 文件加载到 JVM,形成 Class 对象,供程序运行时使用。
分为三个阶段:加载(Loading)→ 链接(Linking)→ 初始化(Initialization)。

加载(Loading)

  • 定位字节码:通过类全限定名(如 java.lang.Object)查找 .class 文件(可从文件、网络、JAR包等来源加载)。
  • 存储数据结构:将字节码解析为方法区(元空间)的运行时数据结构(类信息、常量池、字段、方法等)。
  • 生成 Class 对象:在堆中创建 java.lang.Class 对象,作为访问方法区数据的入口。
  • 双亲委派:类加载器优先委派父加载器加载,避免重复加载(安全性和一致性保障)。

链接(Linking)

  • 验证:确保字节码合法(文件格式、元数据语义、字节码逻辑、符号引用可访问性)。
  • 准备:为静态变量分配内存并赋零值(如 int 为 0),static final 常量直接赋最终值。
  • 解析:将常量池中的符号引用(如类/方法名)转为直接引用(内存地址或句柄)。

初始化(Initialization)

  • 执行 () 方法(编译器自动生成,合并所有静态变量赋值和 static{} 静态代码块)。
  • 触发条件:首次主动使用类时(如 new、访问静态字段/方法、反射、子类初始化等)。
  • 线程安全:JVM 加锁保证 () 只执行一次,且父类先于子类初始化。

7.3丶垃圾回收

1、简述Java垃圾回收机制?(GC是什么?为什么要GC)⭐⭐⭐⭐⭐

Java垃圾回收机制(GC)是JVM自动管理内存的机制,负责回收不再使用的对象,释放内存空间。

目的:

  • 避免内存泄漏:若对象不再使用但未被回收,会导致内存占用持续增长,最终引发OutOfMemoryError。
  • 简化内存管理:开发者无需手动编写free()或delete()代码,降低了内存泄漏的风险
  • 提高内存利用率:GC 会整理内存碎片,使堆内存空间更连续

工作原理:

  • 标记垃圾算法:通过可达性分析算法或引用计数算法找到不再使用的对象。
  • 回收垃圾算法:使用标记-清除、标记-整理、标记-复制算法等回收内存。
  • 分代回收策略:年轻代频繁回收,老年代较少回收。年轻代内存不足时触发Minor GC,老年代内存不足时触发Full GC。
2、标记垃圾的算法⭐⭐⭐⭐
  • 引用计数法:为每个对象维护引用计数器,记录有多少个变量引用该对象。无法解决循环引用问题。
  • 可达性分析算法:从GC Roots出发,遍历所有可达对象,未遍历到的对象被视为垃圾(JVM默认使用)。
    GC Roots 包括:
    1)虚拟机栈(栈帧中的本地变量表) 中引用的对象:当前正在执行的方法中的局部变量(包括参数)所引用的对象。
    2)本地方法栈中JNI引用的对象:当前正在执行的Native方法(用C/C++等写的本地方法)中引用的Java对象。
    3)方法区(或元空间)中类静态属性引用的对象: 类的static变量(静态变量)所引用的对象。
    4)方法区(或元空间)中常量引用的对象:被声明为static final的常量所引用的对象(通常是字符串常量池里的字符串或基本类型包装类的缓存对象)。
    5)Java虚拟机内部的引用:虚拟机自身运行所需的一些内部对象引用,基本数据类型对应的Class对象(如Integer.TYPE)。系统类加载器(System Class Loader)。一些常驻的异常对象(如NullPointerException, OutOfMemoryError)。 预加载的核心类库中的对象。
    6)被同步锁(synchronized)持有的对象:当前正被用作监视器锁(Monitor) 的对象。
    7)反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等:一些用于监控、调试、性能分析等管理接口相关的对象。
    重点: 两栈(虚拟机栈+本地方法栈)+两方法区(类静态变量+常量)
3、JVM 垃圾回收算法? ⭐⭐⭐⭐⭐

标记-清除算法

  • 过程:标记所有可达对象,清除未标记对象。
  • 优点:简单高效。
  • 缺点:产生内存碎片。

标记-整理算法

  • 过程:标记所有可达对象,将存活对象向内存一端移动,清理边界以外的内存。
  • 优点:无内存碎片。
  • 缺点:对象移动开销大,效率较低。

标记-复制算法

  • 过程:将内存分为两块,复制存活对象到另一块内存,清空当前内存。
  • 优点:无内存碎片,效率高。
  • 缺点:内存利用率低。

分代回收策略:

  • 年轻代:使用复制算法,适合生命周期短的对象。
  • 老年代:使用标记-清除或标记-整理算法,适合生命周期长的对象。
4、JVM中的分代回收 ⭐⭐⭐⭐⭐

JVM中的分代回收基于对象的生命周期,将堆内存分为新生代和老年代,采用不同的垃圾回收策略。

堆的区域划分:

  • 新生代:占堆内存的1/3,分为Eden区(80%)和Survivor区(From和To,各10%)。
  • 老年代:占堆内存的2/3,存放生命周期较长的对象。

分代回收策略:

  • 对象分配:新创建的对象首先分配在Eden区。
  • Minor GC:当Eden区内存不足时触发,将存活对象复制到Survivor区(To),清空Eden区和Survivor区(From),交换From和To的角色。
  • 对象晋升:对象在Survivor区经过多次Minor GC后(默认15次)仍存活,晋升到老年代。

优点:

  • 提高垃圾回收效率,减少Full GC的频率。
5、MinorGc、Mixed Gc、FullGC的区别是什么 ⭐⭐⭐⭐⭐

Minor GC:

  • 范围:仅回收新生代(Eden区和Survivor区)。

  • 触发条件:Eden区内存不足时触发。

  • 特点:暂停时间短,使用复制算法,效率高。

Mixed GC:

  • 范围:回收新生代和老年代的部分区域,G1垃圾回收器特有。
  • 触发条件:老年代占用率达到一定阈值时触发。
  • 特点:选择性回收老年代,减少Full GC的频率,暂停时间介于Minor GC和Full GC之间。

Full GC:

  • 范围:回收整个堆内存(新生代和老年代)及方法区(元空间)。
  • 触发条件:老年代或方法区内存不足,或显式调用System.gc()。
  • 特点:暂停时间长,对程序性能影响较大,应尽力避免。
6、 JVM 有哪些垃圾回收器? ⭐⭐⭐⭐⭐

新生代

  • Serial GC :采用复制算法,单线程执行。 特点:简单高效,适用于单线程环境
  • ParNew GC:采用复制算法,多线程并行执行。是 Serial GC 的多线程版本,主要与 CMS GC 配合使用
  • Parallel Scavenge GC:采用复制算法,多线程并行执行。吞吐量优先,自动调整堆大小以达到预设的吞吐量目标

老年代

  • Serial Old GC:采用标记 - 整理算法,单线程执行。用于配合 Serial GC 或作为 CMS GC 的后备方案
  • Parallel Old GC:采用标记 - 整理算法,多线程并行执行。与 Parallel Scavenge GC
    配合,实现全堆的吞吐量优先回收。
  • CMS GC:采用标记 - 清除(为了低延迟)算法,多阶段与用户线程并发执行。以最短回收停顿时间为目标,通过并发标记和清除减少 STW 时间,但可能产生内存碎片。运作阶段分为:
    初始标记:GC Roots直接关联的对象,速度很快,需要停顿
    并发标记:从GC Roots直接关联的对象开始变量整个对象图,不需要停顿
    重新标记:标记那些由于程序运作产生变动过的对象,需要停顿
    并发清除:标记-清除掉已经死亡的线程,不需要停顿

全堆垃圾回收器

  • G1 GC:将堆划分为多个大小相等的Region,混合使用标记-复制算法和标记 - 整理算法。兼顾吞吐量和低延迟,优先回收垃圾最多的 Region(Garbage-First 策略),适用于大内存场景。

G1(Garbage-First)是JDK 7u4引入、JDK 9成为默认的垃圾回收器,目标是在可控的停顿时间(STW)内实现高吞吐量,特别适合大内存且对延迟敏感的应用场景。其核心特点:

  • 内存划分:将堆分为多个大小相等的Region,动态充当 Eden、Survivor、Old 或 Humongous(大对象)区。

  • 核心目标:G1的核心设计目标是允许用户设置期望的最大停顿时间目标(通过 -XX:MaxGCPauseMillis= 指定,如200ms)。,在吞吐量和低延迟间平衡。

  • 工作阶段:
    Young GC:
    触发: Eden区Region满了。
    过程:暂停应用线程,采用复制算法,将Eden和一个Survivor区(From)中的存活对象复制到另一个Survivor区(To)或直接晋升到Old区的Region中。回收的是整个年轻代的Region(Eden + Survivor)
    Mixed GC:
    触发:老年代占比超阈值(默认45%)且并发标记完成。
    过程:初始标记 (停顿): 借Young GC停顿标记直接引用。
    并发标记 (不停顿):与应用线程并发,标记全堆可达对象。
    最终标记 (停顿): 处理并发期间的引用变化。
    混合回收 (停顿): 选择一组包含Young和部分高价值Old Region进行回收(复制算法)。避免全堆扫描!

  • 大对象处理:Humongous 区存储≥Region 一半大小的对象,避免频繁 GC

  • 注意:G1 垃圾回收器既不是单纯基于标记 - 清除,也不是单纯基于标记 - 复制。从整体来看,G1 是基于标记 -整理算法实现的收集器;从局部(两个 Region 之间)上来看,是基于 “标记 - 复制” 算法实现

  • 简要对比CMS的优势:内存碎片控制、可预测停顿、全功能回收

ZGC
由 Oracle 主导、JDK15 正式转正的超低延迟并发垃圾回收器,核心依托染色指针技术实现亚毫秒级停顿(目标始终小于 10 毫秒),停顿时间与堆大小、存活对象数无关,JDK17 引入分代支持,适配 TB 级甚至 PB 级大内存场景但平台支持相对有限。

Shenandoah GC
由 RedHat 主导、JDK17 正式转正的低延迟并发垃圾回收器,通过疏散指针与写屏障实现几乎全程的并发回收,停顿时间不随堆内存大小增长而增加,跨平台性更好且默认无分代(可选分代)。

7、强引用、软引用、弱引用、虚引用的区别? ⭐⭐⭐⭐
  • 强引用:只要强引用存在,对象就不会被回收。即使内存不足,JVM抛出OutOfMemoryError也不会回收强引用对象。用于表示程序中必须存活的对象。
  • 软引用:通过SoftReference类实现。当内存不足时,JVM会回收软引用对象。
  • 弱引用:通过WeakReference类实现。只要发生垃圾回收,无论内存是否充足,弱引用对象都会被回收。适用于那些希望在对象不再被使用时尽快被回收的场景。
  • 虚引用:通过PhantomReference类实现。无法通过虚引用来获得对象实例,为一个对象设置虚引用唯一目的只是能为了这个对象被垃圾回收器回收时收到一个系统通知。

7.4丶JVM实践

1、JVM 调优的参数在哪里设置,以及调优参数有哪些 ⭐⭐⭐
  • tomcat的设置vm参数
    修改TOMCAT_HOME/bin/catalina.sh文件,如下图
    在这里插入图片描述
  • jar包部署在启动参数设置:java -Xms512m -Xmx1024m -jar xxxxjar

调优参数

  • 设置堆空间大小

  • 虚拟机栈的大小

  • 年轻代中Eden区和两个Survivor区的大小比例

  • 年轻代晋升老年代阈值

  • 设置垃圾回收收集器

2、JVM调优的工具 ⭐⭐⭐

命令工具

  • jps:查看当前运行的 Java 进程 ID(类似 Linux 的ps命令)
  • jstack:打印 Java 进程中线程的堆栈信息,用于排查线程阻塞、死锁或异常栈
  • jmap:生成堆内存转储文件(Heap Dump),或查看堆内存使用情况(如对象分布)
  • jhat:分析堆转储文件(Heap Dump),提供网页版的对象统计和引用分析
  • jstat:实时监控 JVM 的 GC 频率、内存分区使用量等统计数据

可视化工具

  • jconsole:通过图形界面监控 JVM 的内存、线程、类加载、CPU 使用率等指标。
  • VisualVM::集成监控、分析、调试于一体,支持实时内存 / 线程监控、Heap Dump 分析、性能调优等。
3、Java内存泄露的排查思路 ⭐⭐⭐
  • 监控内存使用情况: 使用jstat等工具监控堆内存的使用情况,观察内存占用是否持续增长。
  • 获取堆内存快照:使用jmap命令生成堆内存快照:或在JVM启动参数中添加-XX:+HeapDumpOnOutOfMemoryError,在内存溢出时自动生成堆内存快照。
  • 分析堆内存快照: 使用VisualVM等工具加载堆内存快照,查找占用内存最多的对象,分析其引用链。
  • 定位问题代码: 根据对象引用链,找到对应的代码位置,通过阅读上下文的情况,进行修复即可
4、CPU飙高排查方案与思路? ⭐⭐⭐

1.使用top命令查看占用cpu的情况,查看是哪一个进程占用cpu较高
在这里插入图片描述

2.使用ps命令查看进程中的线程信息,看看哪个线程的cpu占用较高
在这里插入图片描述

3.使用jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号
在这里插入图片描述

八、计算机网络

8.1、TCP协议

1、描述TCP三次握手和四次挥手的过程? ⭐⭐⭐⭐⭐

三次握手

  • 第一次握手:客户端发送一个带有 SYN=1(同步序列号)标志和随机生成的初始序列号seq=x的TCP报文段,客户端进入 SYN_SENT 状态,等待服务器确认。
  • 第二次握手:服务器接收到SYN报文段后,回复SYN=1、ACK=1(确认标志位),并发送自己的初始序列号seq=y,以及确认号ack=x+1(表示期望下次收到x+1),服务器进入SYN_RCVD 状态。
  • 第三次握手:客户端收到服务器的 SYN + ACK 报文段后,向服务器发送一个 ACK=1,确认号ack=y+1,序列号seq=x+1的报文段。双方进入ESTABLISHED状态,连接建立完成。第三次握手可携带数据。

四次挥手

  • 第一次挥手:主动关闭方(通常是客户端)向对方发送FIN=1和序列号seq=u,用来关闭主动方到被动方的数据传送,主动方进入FIN_WAIT_1 状态。表示不再发送数据,但可接收数据。
  • 第二次挥手:被动关闭方(如服务端)收到 FIN 报文段后,回复ACK=1和确认号ack=u+1,序列号seq=v,被动方进入 CLOSE_WAIT状态,主动方收到 ACK 报文段后,进入 FIN_WAIT_2 状态。此时服务端可能仍有未发送完的数据
  • 第三次挥手:服务端数据发送完毕后,发送FIN=1和ACK=1,序列号seq=w,确认号ack=u+1。服务端进入LAST_ACK状态。
  • 第四次挥手:客户端收到FIN后,回复ACK=1和确认号ack=w+1,序列号seq=u+1。客户端进入TIME_WAIT状态(等待2MSL时间确保服务端收到ACK),最终关闭连接。服务端收到ACK后立即关闭。
2、为什么需要三次握手? ⭐⭐⭐⭐⭐
  • 确保双方准备就绪:通过三次握手,客户端和服务器都能确认对方具有接收和发送数据的能力,并且都准备好了进行数据传输。例如,在第二次握手中,服务器向客户端发送SYN + ACK 报文段,客户端收到后就知道服务器已经准备好了;而服务器通过第三次握手收到客户端的 ACK报文段,也知道客户端准备好了。
  • 防止历史连接初始化:可以避免因网络延迟等原因导致的旧连接请求报文段被误处理。如果只有两次握手,当客户端发送的旧连接请求报文段在网络中滞留一段时间后到达服务器,服务器误认为是新的连接请求并发送确认报文段,而客户端此时可能已经不再需要建立连接,就会导致错误的连接建立。
3、TIME_WAIT状态的作用? ⭐⭐⭐
  • 保证最后一个 ACK 报文段能被对方收到:如果主动方发送完 ACK 报文段后直接关闭连接,而该 ACK报文段在传输过程中丢失,被动方就会重发 FIN 报文段。处于 TIME_WAIT 状态的主动方能够接收重发的 FIN 报文段,并重新发送ACK 报文段,从而确保连接的可靠关闭。
  • 等待一段时间让网络中所有与该连接相关的报文段都消失:TIME_WAIT 状态的持续时间通常为2MSL,这可以保证在这段时间内,网络中所有与该连接相关的报文段都已经被丢弃,避免新的连接收到旧连接的报文段而产生错误。
4. TCP和UDP的核心区别?各自的应用场景? ⭐⭐⭐⭐⭐
对比维度 TCP UDP
连接方式 面向连接(三次握手、四次挥手) 无连接,直接发送数据
可靠性 可靠传输(确认、重传、排序、流量控制) 不可靠,不保证数据到达或顺序正确
数据顺序 保证数据按序到达 不保证顺序
流量控制 通过滑动窗口机制动态调整发送速率 无控制,可能丢包或拥塞
拥塞控制 有(如慢启动、拥塞避免算法)
头部开销 较大(至少20字节,含序列号、确认号等字段) 较小(仅8字节,含源/目标端口和长度校验和)
传输效率 低(需维护连接状态和确认机制) 高(无连接管理,直接发送)
适用场景 对可靠性要求高的应用 对实时性要求高的应用

TCP应用场景

  • Web浏览(HTTP/HTTPS) 需要可靠传输网页内容,确保数据完整无误。
  • 文件传输(FTP/SFTP) 文件必须完整到达,避免损坏或丢失。
  • 电子邮件(SMTP/POP3/IMAP) 邮件内容需严格按序传递。

UDP应用场景

  • 实时音视频(直播流) 允许少量丢包,但要求低延迟(如视频会议)。
  • 在线游戏(MOBA、FPS) 游戏状态需快速更新,延迟比丢包更影响体验。
  • DNS查询 请求-响应模式简单,一次查询只需一个报文。
5、 TCP如何实现数据的可靠传输? ⭐⭐⭐⭐
  • 确认应答(ACK):接收方收到数据后返回ACK。
  • 超时重传:未收到ACK则重发数据包。
  • 序列号与确认号:保证数据有序性和完整性。
  • 流量控制:通过滑动窗口机制动态调整发送速率。

8.2丶HTTP协议

6. HTTP和HTTPS的区别? ⭐⭐⭐⭐⭐
对比项 HTTP HTTPS
协议 明文传输(无加密) 加密传输(SSL/TLS)
默认端口 80 443
安全性 数据可被窃听、篡改、劫持 数据加密,防止窃听、篡改、劫持
证书 不需要 需要数字证书(CA 颁发)
性能 更快(无加密开销) 稍慢(加密/解密消耗 CPU)
SEO 影响 搜索引擎可能降权 搜索引擎优先推荐(Google 等强制 HTTPS)
适用场景 不敏感数据(如静态网页) 敏感数据(登录、支付、API 通信)
7、HTTPS如何保证安全性? ⭐⭐⭐⭐⭐
  • 数据加密:HTTPS 使用 SSL/TLS 协议对数据进行加密。在握手过程中,客户端和服务器协商出一套加密算法和密钥,之后传输的数据都会用此密钥进行加密。常见的加密算法有对称加密AES、非对称加密RSA 等。这样即使数据在传输过程中被截取,没有正确的密钥也无法解密出原始数据。
  • 身份认证:通过服务器的数字证书来实现。证书中包含了服务器的域名、公钥以及证书颁发机构的签名等信息。客户端收到服务器的证书后,会验证证书的合法性和有效性,包括检查证书是否由受信任的证书颁发机构颁发、证书是否过期以及证书中的域名与访问的域名是否一致等。如果证书验证通过,客户端就可以确认服务器的身份是可信的。
  • 数据完整性保护:HTTPS 使用消息认证码(MAC)或哈希算法来确保数据的完整性。在数据传输过程中,发送方会根据数据内容计算出一个哈希值或 MAC值,并将其与数据一起发送。接收方收到数据后,会重新计算哈希值或 MAC值,并与接收到的值进行比较。如果两者一致,就说明数据在传输过程中没有被篡改;否则,就认为数据已被破坏,会拒绝接收或要求重新传输。
8、HTTP常见状态码 ⭐⭐⭐⭐
类别 含义
1xx 信息性状态码(请求已被接收,继续处理)
2xx 成功状态码
3xx 重定向状态码
4xx 客户端错误状态码
5xx 服务器错误状态码
状态码 名称 为什么重要?(面试高频考点)
200 OK 所有成功请求的基础,RESTful API设计规范必考点,常结合接口返回格式、幂等性设计提问。
404 Not Found 面试必考!常问“如何排查404?”“Spring中如何自定义404页面/响应体?”“Nginx如何配置404重定向?”
500 Internal Server Error Java后端核心考点!面试官会问“如何通过日志排查500错误?”“全局异常处理如何设计?”“OOM时的排查流程?”
400 Bad Request 参数校验相关,常结合Spring的@Valid、Hibernate Validator提问,涉及“前后端校验的区别与场景”。
401 Unauthorized 认证失败场景(如JWT/Token失效),高频对比考点:与403的区别(未认证 vs 无权限),常问“Token刷新机制”。
403 Forbidden 权限控制核心!涉及Shiro/Spring Security框架应用,常问“RBAC模型中如何返回403?”“接口级权限校验实现”。

8.3丶其他问题

9、浏览器输入URL后的全过程 ⭐⭐⭐⭐⭐
  1. DNS解析:将域名转换为IP地址(浏览器缓存→本地Host→DNS服务器)。
  2. TCP连接:与服务器三次握手建立连接。
  3. 发送HTTP请求:浏览器发送HTTP报文(GET/POST)。
  4. 服务器处理请求:返回响应(HTML/CSS/JS等资源)。
  5. 浏览器渲染:解析HTML构建DOM树,加载CSS/JS,渲染页面。
  6. 断开连接:四次挥手释放TCP连接(若非Keep-Alive)

九、设计模式

9.1丶单例模式

1、 单例模式的作用及其使用场景? ⭐⭐⭐⭐⭐

单例模式保证一个类仅有一个实例,并提供一个全局访问点来访问这个实例。
作用

  • 保证实例唯一性:在整个应用程序中,无论在何处访问该类的实例,都是同一个对象,避免了因创建多个实例而导致的资源浪费和数据不一致问题。
  • 提供全局访问点:方便在不同的模块、类之间共享数据和进行交互,使得系统中的各个部分能够方便地访问到同一个实例,提高了代码的可维护性和可扩展性。

场景

  • 配置文件管理:应用程序通常需要读取配置文件中的信息,如数据库连接参数、系统参数等。使用单例模式可以创建一个配置管理类,在程序启动时加载配置文件,并将配置信息存储在单例实例中。这样,在应用程序的任何地方都可以通过该单例实例获取配置信息,保证了配置信息的一致性和全局可访问性。
  • 日志记录:日志记录类可以使用单例模式,确保在整个应用程序中只有一个日志记录器实例。多个模块可以将日志信息发送到同一个日志记录器中,方便对日志进行统一的管理和记录,避免了多个日志记录器可能导致的日志混乱和资源浪费。
  • 数据库连接池:数据库连接池用于管理数据库连接,以提高数据库访问性能和资源利用率。通过单例模式创建一个数据库连接池实例,可以在整个应用程序中共享该连接池,各个模块需要访问数据库时,都从这个单例的连接池中获取数据库连接,从而有效地管理数据库连接资源,避免了频繁创建和销毁数据库连接带来的性能开销。
2、单例模式的几种实现方式,并说明其优缺点 ⭐⭐⭐⭐⭐
  1. 饿汉式
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {} // 私有构造
    public static Singleton getInstance() {
        return instance;
    }
}

优点:线程安全:实例在类加载时创建,避免多线程问题。实现简单:代码简洁,无同步开销。
缺点:可能浪费资源:即使未使用也会提前创建实例

  1. 懒汉式(非线程安全)
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 线程不安全!
        }
        return instance;
    }
}

优点:延迟加载:只有调用 getInstance() 时才创建实例。
缺点:线程不安全:多线程下可能创建多个实例。

  1. 懒汉式 + 同步(线程安全但低效)
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点:线程安全:通过 synchronized 保证单例。
缺点:性能差:每次调用 getInstance() 都加锁,影响并发。

4.双重检查锁(推荐)

public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {                     // 第一次检查
            synchronized (Singleton.class) {        // 加锁
                if (instance == null) {             // 第二次检查
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  • 优点:线程安全:通过双重检查和 volatile 防止指令重排序。高性能:只有第一次创建时加锁,后续调用无锁。
  • 缺点:代码稍复杂:需注意 volatile 和指令重排序问题。
  • 第一次检查(无锁)
    目的是避免不必要的同步 如果实例已经存在,直接返回,不需要进入同步块
    这大大提高了性能,因为大多数情况下实例已经创建
  • 第二次检查(有锁)
    防止"先检查后创建"的竞态条件
    可能在第一个线程创建实例的过程中,其他线程通过了第一次检查在等待锁
    获得锁后需要再次检查实例是否已被创建
  • 为什么加 volatile?
    instance = new Singleton()不是原子操作,可能因指令重排序导致其他线程获取未初始化的对象。分配内存 → 调用构造函数 → 赋值给引用
    没有volatile,JVM可能重排序为:分配内存 → 赋值给引用 → 调用构造函数
    这会导致其他线程可能拿到未完全初始化的对象
  1. 静态内部类(推荐)
public class Singleton {
    private static class Holder {
        static final Singleton instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return Holder.instance;
    }
}

优点:线程安全:利用类加载机制保证单例。延迟加载:只有调用 getInstance() 时才会加载 Holder 类。无同步开销:无需加锁。
缺点:无法传参:如果实例化需要参数,此方式不适用。

  1. 枚举单例(Enum,最安全)
public enum Singleton {
    INSTANCE; // 单例实例
}

优点:绝对线程安全:枚举由JVM保证单例。防反射/反序列化破坏:其他方式可能被反射或反序列化破坏单例,枚举天然免疫。
缺点:不够灵活:无法继承其他类(枚举已继承 Enum 类)。

9.2丶工厂模式

3、工厂模式的作用是什么?哪里用到了工厂模式? ⭐⭐⭐⭐⭐

工厂模式的核心作用是解耦对象的创建与使用,通过统一的接口或方法实例化对象,而不是直接调用 new。它的主要价值包括:

  • 隐藏创建细节:封装复杂对象的构造逻辑(如依赖组装、配置读取)。
  • 降低耦合:调用方只需依赖接口,无需绑定具体实现类。
  • 支持扩展:通过工厂可以灵活切换实现,符合开闭原则。

Spring的BeanFactory是工厂模式的典型应用

  • BeanFactory:Spring 最底层的工厂接口,负责创建和管理 Bean 实例。
  • ApplicationContext:BeanFactory 的子接口,扩展了更多企业级功能(如国际化、事件发布等),是实际使用的工厂。
// 1. 使用 BeanFactory(已过时,但体现工厂模式)
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
UserService userService = factory.getBean(UserService.class);

// 2. 使用 ApplicationContext(推荐)
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = context.getBean(UserService.class);

9.3丶代理模式

4、静态代理和动态代理的区别?⭐⭐⭐⭐⭐

实现方式:

  • 静态代理需要手动编写代理类,在编译期就确定了代理关系
  • 动态代理是在运行时动态生成代理类,主要有两种实现:JDK动态代理(基于接口)CGLIB动态代理(基于继承)
    使用特点:
  • 静态代理一个代理类只能代理一个目标类,代码冗余但性能更好
  • 动态代理一个代理类可以代理多个目标类,更加灵活

适用场景:

  • 静态代理适合简单、固定的代理需求
  • 动态代理适合需要统一管理多个类代理逻辑的复杂场景,比如Spring AOP就是基于动态代理实现的

性能对比:

  • 静态代理直接调用目标方法,性能更高
  • 动态代理通过反射或字节码增强实现,会有轻微性能损耗
5、JDK动态代理和CGLIB动态代理的区别? ⭐⭐⭐⭐⭐

JDK动态代理基于接口,通过反射生成代理类,要求目标类必须实现接口;CGLIB基于继承,通过字节码生成子类,可以代理普通类。
JDK代理是Java原生支持,但性能略低;CGLIB需要第三方库,但效率更高(除了首次加载)。
Spring AOP默认对接口用JDK代理,对无接口类用CGLIB,也可以通过配置强制使用CGLIB。"

6、代理模式在Spring中的应用场景? ⭐⭐⭐⭐⭐
  • AOP:通过JDK/CGLIB代理实现日志、事务等横切逻辑;
  • 声明式事务:@Transactional通过代理管理事务生命周期;
  • 缓存控制:@Cacheable代理拦截方法调用缓存结果;

十、消息中间件

10.1、基础概念

1、消息队列怎么选型? ⭐️⭐️⭐️

核心对比(Kafka vs RabbitMQ vs RocketMQ)

对比项 Kafka RabbitMQ RocketMQ
定位 高吞吐、分布式流处理 通用消息代理 金融级高可靠消息队列
吞吐量 极高(10万+/秒) 中等(1万~5万/秒) 高(5万+/秒)
延迟 毫秒级(高吞吐时更高) 微秒级(最低延迟) 毫秒级
消息顺序 分区内有序 无序 队列内有序
事务支持 支持(但较复杂) 不支持 完整支持(金融级)
消息回溯 支持(可重放历史数据) 不支持 支持(可回溯消费)
消息可靠性 高(但可能丢少量数据) 高(ACK机制) 最高(零丢失)
适用场景 日志、大数据流处理 企业应用集成、低延迟任务 电商、支付、金融交易

1. Kafka(大数据、日志流处理首选)

  • 优点
    • 超高吞吐,适合大数据场景(如日志收集、监控数据)
    • 分区有序,适合需要顺序消费的场景(如订单流水)
    • 持久化强,数据可长期存储,支持回溯消费
  • 缺点
    • 延迟较高,不适合超低延迟场景
    • 配置复杂,依赖ZooKeeper,运维成本高

2. RabbitMQ(企业级、低延迟首选)

  • 优点
    • 延迟最低(微秒级),适合实时任务(如秒杀)
    • 协议丰富(AMQP/MQTT/STOMP),适合企业集成
    • 管理界面完善,运维友好
  • 缺点
    • 吞吐较低,不适合大数据场景
    • 消息无序,无法保证顺序消费

3. RocketMQ(金融级、高可靠首选)

  • 优点
    • 高吞吐 + 高可靠,适合金融、电商场景
    • 支持事务消息(如分布式事务)
    • 支持消息回溯(可重新消费历史数据)
  • 缺点
    • 阿里系技术,社区生态略弱于Kafka
    • 配置较复杂,NameServer协调机制

最终选型建议(面试回答模板)

  • 大数据、日志流处理 → Kafka
  • 低延迟、企业应用集成 → RabbitMQ
  • 金融支付、电商交易 → RocketMQ
2、消息队列使用场景有哪些? ⭐️⭐️⭐️⭐️
  1. 异步处理
    场景描述:将耗时操作异步化,避免阻塞主流程,提升用户体验和系统吞吐量。
    例子:
    用户注册后发送邮件/短信通知。
    电商下单后异步生成订单日志或更新库存。
    视频上传后异步转码处理。
    优势:
    主流程快速响应(如注册成功立即返回,无需等待邮件发送完成)。
    失败可重试(消息队列保证最终一致性)。

  2. 系统解耦
    场景描述:不同服务间通过消息队列通信,降低直接依赖,避免“牵一发而动全身”。
    例子:
    订单系统生成订单后,通过消息队列通知库存系统、物流系统、积分系统,各系统独立消费。
    微服务架构中,服务A通过消息触发服务B的逻辑,而非直接调用API。
    优势:
    系统可独立扩展和升级。
    避免服务间接口耦合导致的级联故障。

  3. 流量削峰
    场景描述:突发流量涌入时,用消息队列缓冲请求,避免后端系统过载。
    例子:
    秒杀活动:将用户请求放入队列,后端按处理能力逐步消费。
    日志收集:高峰期的日志写入队列,下游系统平稳消费。
    优势:
    保护数据库、服务等关键资源。
    平滑处理突发流量(如电商大促)。

3、如何保证消息不丢失?⭐️⭐️⭐️⭐️⭐️

RabbitMQ 消息不丢失的保障机制

  • 生产者端:启用发布确认(Publisher Confirms)异步模式,发消息后监听 Broker 的 ACK/NACK 。收到 NACK或超时没回应,重试或记日志,解决网络波动丢消息问题。
  • Broker 端:全持久化配置。
    交换机持久化:创建时设置 durable=true,确保重启后绑定关系不丢失;
    队列持久化:声明队列时设置 durable=true,防止队列数据丢失;
    消息持久化:发送消息时指定 deliveryMode=2,强制写入磁盘而非仅存于内存。
  • 消费者端:用自动确认(auto)模式,Spring 监听器处理消息正常就自动 ack ;处理异常则触发重试,多次重试失败丢到异常交换机,人工兜底,保障消息完整消费。
4、消息的重复消费问题如何解决? ⭐️⭐️⭐️⭐️⭐️

消息重复消费的原因

  • 生产者重复发送:网络问题导致重试(如 Kafka 生产者重试、RabbitMQ Confirm 超时重发)。
  • Broker 重复投递:消费者处理成功但 ACK 未送达 Broker(如消费者崩溃、网络问题)。
  • 消费者重复拉取:消费者提交 Offset/ACK 失败(如提交前进程挂掉),Broker 再次推送相同消息。

处理方法

  • 基于业务唯一标识:借助业务唯一标识检查数据库。收到消息后,通过标识查询数据库,若数据不存在则处理消息并插入;若已存在,直接忽略,从业务层面避免重复。​
  • Redis 分布式锁或数据库锁:可以利用 Redis 分布式锁或数据库锁。以 Redis分布式锁为例,消费者接收到消息后尝试获取锁,成功则处理消息后释放锁;失败则放弃处理,确保操作幂等,避免重复消费带来的数据不一致问题。
5、高可用机制了解吗?⭐️⭐️⭐️

RabbitMQ
普通集群,具备下面的特征:

  • 会在集群的各个节点间共享部分数据,包括: 交换机、队列元信息。不包含队列中的消息。
  • 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
  • 队列所在节点宕机,队列中的消息就会丢失

镜像集群: 本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个 mq 的镜像节点之间同步备份。
  • 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
  • 一个队列的主节点可能是另一个队列的镜像节点
  • 所有操作都是主节点完成,然后同步给镜像节点
  • 主宕机后,镜像节点会替代成新的主

仲裁队列: 仲裁队列是 3.8 版本以后才有的新功能,用来替代镜像队列,具备下列特征:

  • 与镜像队列一样,都是主从模式,支持主从数据同步
  • 使用非常简单,没有复杂的配置
  • 主从同步基于 Raft 协议,强一致

10.2 RabbitMQ

1、RabbitMQ中死信交换机了解吗? ⭐️⭐️⭐️⭐️

在 RabbitMQ 里,死信交换机是一种特殊的交换机。当消息变成死信时,会被重新路由到死信交换机,再由死信交换机转发到与之绑定的死信队列中。死信队列可用于存储这些异常消息,方便后续排查问题或者进行补偿处理。
消息成为死信的常见原因:

  • 消息被拒绝:消费者使用 basic.reject 或者 basic.nack 方法拒绝消息,并且 requeue 参数设置为 false,即不将消息重新放回队列。
  • 消息过期:消息在队列中的存活时间超过了设置的过期时间
  • 队列达到最大长度:当队列中的消息数量达到了预设的最大长度,新进入的消息就可能成为死信。
2、RabbitMQ延迟队列有了解过吗? ⭐️⭐️⭐️⭐️

延迟队列指的是消息在发送之后,不会立即被消费者消费,而是要等到指定的时间之后才会被消费。在 RabbitMQ 里,并没有直接提供延迟队列的功能,不过可以借助消息的 TTL 和死信交换机来实现延迟队列的效果。
实现原理

  • 设置消息的 TTL:生产者在发送消息时,为消息设置一个 TTL。
  • 绑定死信交换机:将队列绑定到死信交换机。
  • 消息过期处理:当消息在队列中达到 TTL时,消息会变成死信,被路由到死信交换机,再由死信交换机转发到死信队列。消费者监听死信队列,从而实现消息的延迟消费。

另一种延迟队列实现方法:

  • 官方延迟插件:DelayExchange,直接指定消息延迟时间(推荐方案)。

实际项目中常用于订单超时关闭、异步任务延迟执行等场景。"

3、如果有100万消息堆积在MQ,如何解决? ⭐️⭐️⭐️⭐️
  • 提高消费者消费能力:使用多线程是一种提高消费者消费能力的方式,它可以让消费者在同一时间内处理多个消息,从而加快消息的处理速度。但在使用多线程时,需要注意线程安全问题,避免多个线程同时访问和修改共享资源,导致数据不一致等问题。
  • 增加消费者数量:采用工作队列模式,让多个消费者并行消费同一队列,能够有效提高消息的消费速度,减少消息堆积。不过,增加消费者数量也需要考虑系统资源的限制,如果系统资源不足,增加消费者可能会导致系统性能下降。
  • 扩大队列容量:使用 RabbitMQ的惰性队列是一个不错的选择,它能够支持数百万条消息的存储,将消息直接存盘而不是内存,避免了因内存不足导致的消息丢失或系统崩溃。然而,仅仅扩大队列容量并不能从根本上解决消息堆积问题,只是让队列能够暂时容纳更多的消息,还需要结合其他方法来提高消息的处理速度。

10.3 Kafka

1、Kafka是如何保证消费的顺序性?⭐️⭐️⭐️⭐️⭐️
2、Kafka中新增或下线消费者时,分区如何重新分配?⭐️⭐️⭐️⭐️⭐️

答案要点:

触发条件:消费者数量变化、订阅Topic变化、Partition数量变化

分区分配策略:Range(按范围)、RoundRobin(轮询)、Sticky(粘滞,避免全量重排)

面试坑点:Rebalance期间服务不可用,需避免频繁触发

3、Kafka中实现高性能的设计有了解过吗?⭐️⭐️⭐️⭐️⭐️
4、Kafka数据清理机制了解吗? ⭐️⭐️

Kafka通过日志分段(Log Segment)和清理策略管理数据:
基于时间:log.retention.hours(默认168小时/7天)自动删除旧数据。
基于大小:log.retention.bytes限制分区总大小,超出时删除旧数据。
基于压缩(Compact):对Key相同的消息只保留最新值(适用于状态更新场景)。

十一、容器

11.1丶Docker

1、什么是 Docker?它与虚拟机的本质区别是什么?⭐️⭐️⭐️⭐️⭐️

Docker是一个开源的应用容器引擎,基于Go语言开发。它允许开发者将应用及其依赖(如库、环境配置)打包到一个轻量级、可移植的容器中,然后可以在任何安装了Docker的机器上运行。
本质区别:
虚拟化层级:

-虚拟机:虚拟化的是硬件,每个VM包含一整套完整的客户机操作系统。

  • Docker:虚拟化的是操作系统,所有容器共享主机的操作系统内核,但拥有独立的用户空间。

性能与资源:

  • Docker容器是进程级的,启动秒级,资源占用少(只需存储应用及其依赖)。
  • 虚拟机是操作系统级的,启动分钟级,资源占用大(需要为每个VM分配完整的OS资源)。

隔离性:

  • 虚拟机提供了更强的隔离性,每个VM有独立的内核,一个VM崩溃不影响其他VM或主机。
  • Docker容器共享内核,隔离性相对较弱(但通过Namespace和Cgroups技术,对于大多数应用场景已足够安全)。
2、Docker常用命令有哪些? ⭐️⭐️⭐️⭐️⭐️
命令 关键参数 作用 示例
docker run -d, -p, -v, -it, --name 创建并运行容器 docker run -d -p 80:80 --name web nginx
docker ps -a 查看容器(-a 查看所有) docker ps -a
docker exec -it 进入运行中的容器 docker exec -it web bash
docker build -t 构建镜像 docker build -t my-app .
docker images 查看本地镜像 docker images
docker pull 拉取镜像 docker pull redis:alpine
docker stop 停止容器 docker stop web
docker start 启动已停止的容器 docker start web
docker logs -f 查看容器日志(-f 实时跟踪) docker logs -f web
docker rm / docker rmi 删除容器 / 删除镜像 docker rm web
3、Docker 的三个核心概念:镜像、容器、仓库,请解释它们的关系。 ⭐️⭐️⭐️⭐️⭐️
  • 镜像:是一个只读的模板,类似于面向对象中的“类”。它包含了运行应用所需的代码、运行时、库、环境变量和配置文件。
  • 容器:是镜像的一个运行实例,类似于“对象”。容器可以被创建、启动、停止、删除。容器在镜像的只读层上创建一个可写层,使得应用可以运行。
  • 仓库:是集中存放镜像文件的地方,类似于代码仓库(如Git)。最著名的公共仓库是Docker Hub。
  • 关系:程序员通过Dockerfile定义一个镜像,并将其推送到仓库。在需要部署时,从仓库拉取镜像,然后通过docker run命令,从镜像创建并启动一个容器。
4、什么是Dockerfile?常用语法有哪些⭐️⭐️⭐️⭐️⭐️

Dockerfile:是一个文本文件,里面包含了一条条的指令(Instruction),每一条指令构建一层。Docker引擎通过读取Dockerfile中的指令自动生成镜像。

指令 作用 示例
FROM 指定基础镜像(必须的第一个指令) FROM node:16-alpine
WORKDIR 设置工作目录(后续命令都在此目录执行) WORKDIR /app
COPY 复制文件(从宿主机复制到镜像) COPY . .
RUN 执行命令(安装依赖、编译等) RUN npm install
EXPOSE 声明端口(容器运行时监听端口) EXPOSE 3000
ENV 设置环境变量 ENV NODE_ENV=production
CMD 容器启动命令(只能有一个) CMD [“node”, “app.js”]
5、如何实现 Docker 容器的持久化存储? ⭐️⭐️⭐️⭐️

Docker容器默认的文件系统是临时的,容器删除后数据就丢失了。实现持久化主要有两种方式:

Bind Mount(绑定挂载):将宿主机上的一个特定目录或文件直接挂载到容器中。
命令:docker run -v /host/path:/container/path …
特点:性能好,依赖主机目录结构。

Volume(数据卷):由Docker完全管理的、存储在宿主机上的一部分空间(通常是/var/lib/docker/volumes/下)。
命令:docker volume create my_vol 然后 docker run -v my_vol:/container/path …
特点:推荐方式,易于备份和迁移,与宿主机文件系统解耦。

Tmpfs Mount(内存挂载):将数据存储在宿主机的内存中,不持久化。(了解即可)

6、什么是Docker Compose?⭐️⭐️⭐️⭐️

Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。

通过一个docker-compose.yml文件来配置所有应用程序的服务(如Web服务、数据库、缓存等)。

然后,使用一个简单的命令(docker-compose up),就可以创建并启动所有定义的服务。

解决了什么问题:解决了微服务架构中,需要手动多次docker run来启动多个关联容器的麻烦,实现了“一键启停”整个应用栈。

7、Docker 和 Kubernetes 是什么关系? ⭐️⭐️⭐️

Docker 解决了单个容器的打包、分发和运行问题。

Kubernetes 解决了成百上千个容器的编排、调度、服务发现、扩缩容等管理问题。

关系:可以把Docker看作“集装箱”,而Kubernetes就是管理这些集装箱的“全球海运调度系统”。Docker是Kubernetes底层支持的一种容器运行时(虽然现在Kubernetes支持更多运行时,如containerd)。

8、Docker容器之间如何通信? ⭐️⭐️⭐️⭐️

主要有三种方式:

  • 通过IP地址通信:Docker会为每个容器分配一个IP地址,容器之间可以直接通过这个IP进行通信。但不推荐,因为IP是动态分配的。
  • 通过–link连接(已废弃,了解即可):在docker run时通过–link参数将两个容器连接起来,可以通过容器名通信。
  • 通过自定义Docker网络(推荐): 创建自定义网络:docker network create my-network
    将容器加入到同一个网络中:docker run --network=my-network --name=container1 …
    这样,在container1中就可以直接通过容器名container2来访问另一个容器。这是最常用、最推荐的方式。

11.2、Kubernetes

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐