java基础

1.==和equals的区别?

对于 Object 来说,equals 是用 == 实现的,所以二者是相同的,都是用来比较两个对象的引用是否相同的,但 Java 中的其他类,都会重写 equals 让其变为值比较,而非引用比较,如 Integer 和 String 都是这样。

2.方法的重写和重载的区别?

重载:在同一类中

1.方法名必须相同

2.参数类型不同

3.参数数量不同

4.参数顺序不同

5.方法返回值和访问修饰符可以不同

重写:是子类对父类允许访问的方法进行重新编写

1.方法名必须相同

2.参数列表必须相同

3.子类返回值范围应比父类的更小或相等

4.访问修饰符范围应该大于或等于父类

3.抽象类和接口的区别?

1.定义的关键字不同:抽象类为abstract,接口为interface

2.方法:抽象类可以包含抽象方法和具体方法,接口只能包含方法的声明(抽象方法)

3.方法的访问修饰符:抽象类无限制,只是里面的抽象方法不能用private

​ 接口有限制,默认的public方法

4.实现:一个类只能继承一个抽象类但可以实现多个接口

5.变量:抽象类可以包含实例变量和静态变量,接口只能包含常量

6.构造函数:抽象类可以有构造函数,接口不能有构造函数

4.Integer的缓存机制

Integer会缓存-128到127的对象到常量池

之后不论是new还是什么,Integer只要在这个范围内只要值相同,他们都是同一个对象

比如:

Integer a=Integer.valueOf(127);
Integer b=Integer.valueOf(127);
a==b为true

Float和Double没有缓存机制

5.final,finally,finalize的区别?finally中的方法一定会执行吗?

final:

是个修饰符,可以用于修饰类,方法和变量

修饰类时,该类不能被继承,即为最终类

修饰方法时,该方法不能被子类重写

修饰变量时,表示该变量是一个常量,其值不能被修改

finally:

是一个关键字,用于定义一个代码块,通常与try-catch结构一起使用

finally块中的代码无论是否抛出异常,都会被执行

finally块通常用于释放资源、关闭连接或执行必要的清理操作

finalize:

finalize是Object类中的一个方法,被用于垃圾回收机制

finalize方法在对象被垃圾回收之前被调用,用于进行资源释放或其他清理操作

通常情况下,我们不需要显式地调用finalize方法,而是交由垃圾回收器自动调用

finally中的方法一定会执行吗?

正常运行的情况下,finally 中的代码是一定会执行的,但是,如果遇到 System.exit() 方法或 Runtime.getRuntime().halt() 方法,或者是 try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题,finally 中的代码是不会执行的。而 exit() 方法会执行 JVM 关闭钩子方法或终结器,但 halt() 方法并不会执行钩子方法或终结器。

finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行

另外,在以下 2 种特殊情况下,finally 块的代码也不会被执行:

  1. 程序所在的线程死亡。
  2. 关闭 CPU。

不会执行的情况:

1.System.exit()

2.Runtime.getRuntime().halt()

3.try 中发生了死循环、死锁,遇到了掉电、JVM 崩溃等问题

6.多态的原理?

什么是多态?

多态是面向对象编程中的一个重要概念,它允许通过父类类型的引用变量来引用子类对象,并在运行时根据实际对象的类型来确定调用哪个方法。换句话说,一个对象可以根据不同的情况表现出多种形态。

通过多态,我们可以利用父类类型的引用变量来指向子类对象,并根据实际对象的类型调用对应的方法。这样可以在不修改现有代码的情况下,动态地切换和扩展对象的行为。

特点和优势:

  1. 可替换性:子类对象可以随时替代父类对象,向上转型。
  2. 可扩展性:通过添加新的子类,可以扩展系统的功能。
  3. 接口统一性:可以通过父类类型的引用访问子类对象的方法,统一对象的接口。
  4. 代码的灵活性和可维护性:通过多态,可以将代码编写成通用的、松耦合的形式,提高代码的可维护性。

实现原理:

多态的实现原理主要是依靠“动态绑定”和“虚拟方法调用”,它的实现流程如下:

  1. 创建父类类型的引用变量,并将其赋值为子类对象。
  2. 在运行时,通过动态绑定确定引用变量所指向的实际对象的类型。
  3. 根据实际对象的类型,调用相应的方法版本。

动态绑定:

动态绑定(Dynamic Binding):指的是在编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。这意味着,编译器会推迟方法的绑定(即方法的具体调用)到运行时。正是这种动态绑定机制,使得多态成为可能。

虚拟方法调用:

虚拟方法调用(Virtual Method Invocation):在 Java 中,所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。当通过父类类型的引用变量调用被子类重写的方法时,虚拟机会根据实际对象的类型来确定要调用的方法版本,而不是根据引用变量的声明类型。

7.抽象类和普通类的区别?

实例化:普通类可以直接实例化,而抽象类不能直接实例化。

方法:抽象类中既包含抽象方法又可以包含具体的方法,而普通类只能包含普通方法。

实现:普通类实现接口需要重写接口中的方法,而抽象类可以实现接口方法也可以不实现。

8.String,Stringbuffer,StringBuilder的区别

可变性:

  1. String是不可变的类,一旦创建就不能被修改。每次对String进行操作时,都会创建一个新的String对象。
  2. StringBuffer和StringBuilder是可变的类,可以动态修改字符串内容。

线程安全性:

  1. String是线程安全的,因为它是不可变的。多个线程可以同时访问同一个String对象而无需担心数据的修改问题。
  2. StringBuffer是线程安全的,它的方法使用了synchronized关键字进行同步,保证在多线程环境下的安全性。
  3. StringBuilder是非线程安全的,不使用synchronized关键字,所以在多线程环境下使用时需要手动进行同步控制。

性能:

  1. 由于String是不可变的,每次对String进行操作都会创建一个新的String对象,频繁的字符串拼接会导致大量的对象创建和内存消耗。
  2. StringBuffer是可变的,对字符串的修改是在原有对象上进行,不会创建新的对象,因此在频繁的字符串拼接场景下比String更高效。
  3. StringBuilder与StringBuffer类似,但不保证线程安全性,因此在单线程环境下性能更高。

总结:

综上,如果在单线程环境下进行字符串操作,且不需要频繁修改字符串,推荐使用String;如果在多线程环境下进行字符串操作,或者需要频繁修改字符串,优先考虑使用StringBuffer;如果在单线程环境下进行频繁的字符串拼接和修改,推荐使用StringBuilder以获取更好的性能。

9.字符型常量和字符串常量的区别?

形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。

含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。

占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节。

10.ArrayList

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

ArrayList只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。

ArrayList是有序的。

ArrayList是线程不安全的,但是效率高。

ArrayList如果使用无参构造器,初始容量是0,当第一次添加时,容量变为10,当需要扩容时1.5倍扩容。

如果使用指定大小构造器,则初始容量为指定大小,需要扩容时直接1.5倍扩容。

ArrayList与Vector区别?

答:Vector是线程安全的,ArrayList是线程不安全的。

Vector无参构造器时默认是10容量,需要扩容时2倍扩容,指定容量有参构造的话直接为指定大小,2倍扩容

11.ArrayList和LinkedList有什么区别?

  1. 底层数据结构:ArrayList使用==数组来存储元素,而LinkedList使用双向链表==来存储元素。
  2. 随机访问性能:ArrayList支持高效的随机访问(根据索引获取元素),因为它可以通过下标计算元素在数组中的位置。而LinkedList在随机访问方面性能较差,获取元素需要从头或尾部开始遍历链表找到对应位置。
  3. 插入和删除性能:ArrayList在尾部添加或删除元素的性能较好,因为它不涉及数组的移动。而在中间插入或删除元素时,ArrayList涉及到元素的移动,性能相对较低。LinkedList在任意位置进行插入和删除操作的性能较好,因为只需要调整链表中的指针即可。
  4. 内存占用:ArrayList在每个元素都需要存储一个引用和一个额外的数组空间,因此内存占用比较高。而LinkedList由于需要存储前后节点的引用,相对于ArrayList占用的内存更多。

时间复杂度:

ArrayList查询O(1) 插入和删除O(n)

Linkedlist查询O(n) 插入和删除O(1)

12.HashMap

在 JDK 1.7 时,HashMap 底层是通过数组 + 链表实现的;

而在 JDK 1.8 时,HashMap 底层是通过数组 + 链表或红黑树实现的。

链表升级为红黑树:

在 JDK 1.8 之后,HashMap 默认是先使用数组 + 链表存储数据,但当满足以下两个条件时:

  1. 链表的数量大于阈值(默认是 8)
  2. 并且数组长度大于 64 时

为了(查询)的性能考虑会将链表升级为红黑树进行存储,具体执行流程如下:

  1. 创建新的红黑树对象,并将链表内所有的键值对全部添加到红黑树中。
  2. 将原来的链表引用指向新创建的红黑树。

红黑树退化为链表:

当进行了删除操作,导致**红黑树的节点小于等于 6 时,会发生退化**,将红黑树转换为链表。这是因为当节点数量较少时,红黑树对性能的提升并不明显,反而占用了更多的内存空间。具体执行流程如下:

  1. 从红黑树的根节点开始,按照中序遍历的顺序将所有节点加入到一个新的链表中。
  2. 将原来的红黑树引用指向新创建的链表。

扩容机制:

第一次添加时table数组扩容到16,当数组元素到达16*负载因子0.75=12(临界值)时便会对数组进行扩容

扩容是**2倍扩容**

hashset比较添加的元素是否重复是判断**hash()相同且equals()**相同

对于hashmap中链表添加节点是使用头插法,这导致每次扩容后链表里的元素顺序会反转

13.为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

14.HashSet和HashMap有什么区别?

HashSet 实现了 Set 接口,只存储对象;HashMap 实现了 Map 接口,用于存储键值对。

HashSet 底层是用 HashMap 存储的,HashSet 封装了一系列 HashMap 的方法,HashSet 将(自己的)值保存到 HashMap 的 Key 里面了。

HashSet 不允许集合中有重复的值(如果有重复的值,会插入失败),而 HashMap 键不能重复,值可以重复(如果键重复会覆盖原来的值

15.什么是反射?使用场景有哪些?

在 Java 中,反射是指在运行时检查和操作类、接口、字段、方法等程序结构的能力。通过反射,可以在运行时获取类的信息,创建类的实例,调用类的方法,访问和修改类的字段等。

反射使用场景:

编程开发工具的代码提示,如 IDEA 或 Eclipse 等,在写代码时会有代码(属性或方法名)提示,这就是通过反射实现的。

很多知名的框架如 Spring,为了让程序更简洁、更优雅,以及功能更丰富,也会使用到反射,比如 Spring 中的依赖注入就是通过反射实现的。

数据库连接框架也会使用反射来实现调用不同类型的数据库(驱动)。

反射的关键实现方法有以下几个:

得到类:Class.forName("类名")

得到所有字段:getDeclaredFields()

得到所有方法:getDeclaredMethods()

得到构造方法:getDeclaredConstructor()

得到实例:newInstance()

调用方法:invoke()

16.浅克隆和深克隆有什么区别?

深克隆的实现方法有很多,比如以下几个:

  1. 所有引用属性都实现克隆,整个对象就变成了深克隆。
  2. 使用 JDK 自带的字节流序列化和反序列化对象实现深克隆。
  3. 使用第三方工具实现深克隆,比如 Apache Commons Lang。
  4. 使用 JSON 工具,如 GSON、FastJSON、Jackson 序列化和反序列化对象实现深克隆。

17.成员变量和局部变量的区别?

语法形式:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

存储方式:从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。

生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。

默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值,局部变量如果我们不手动赋初始值会报错。

18.java语言有哪些特点?

1.面向对象(封装 继承 多态)

2.平台无关性(java虚拟机实现平台无关性)

3.支持多线程

4.可靠性(具备异常处理和自动内存管理机制)

5.安全性(Java 语言本身的设计就提供了多重安全防护机制如访问权限修饰符、限制程序直接访问操作系统资源)、

6.解释与编译并存(编译阶段:java文件编译成.class字节码文件 解释阶段:JVM加载字节码文件将其解释为机器码,然后在计算机上执行)

JIT:即时编译 有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 JIT(Just in Time Compilation) 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用

19.JDK、JRE、JVM、JIT 这四者的关系?

JDK、JRE、JVM、JIT 这四者的关系

JDK:java开发工具包

包含了 JRE,同时还包含了编译 java 源码的编译器 javac 以及一些其他工具比如 javadoc(文档注释工具)、jdb(调试器)、jconsole(基于 JMX 的可视化监控⼯具)、javap(反编译工具)等等

JRE: java 运行时环境

主要包括 Java 虚拟机(JVM)、Java 基础类库(Class Library)

JVM:java虚拟机

执行Java字节码,将其解释成底层的机器码,然后在宿主机上运行

JIT:

JIT(Just-In-Time)编译器是一种在程序运行时将字节码编译成本地机器码的技术。它是Java虚拟机(JVM)的一部分,用来提高Java程序的执行效率。 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用

20.为什么说 Java 语言“编译与解释并存”?

这是因为 Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(.class 文件),这种字节码必须由 Java 解释器来解释执行。

21.java和c++区别?

22.标识符和关键字的区别?

在我们编写程序的时候,需要大量地为程序、类、变量、方法等取名字,于是就有了 标识符 。简单来说, 标识符就是一个名字

有一些标识符,Java 语言已经赋予了其特殊的含义,只能用于特定的地方,这些特殊的标识符就是 关键字 。简单来说,关键字是被赋予特殊含义的标识符

23.java中有几种基本数据类型了解吗?

8种。

6种数字类型:

​ 4种整数型:byte,shot,int,long(默认值0 0 0 0L)

​ 2中浮点型:float,double(默认值 0.0f 0.0d)

1中字符类型:char(默认值\u0000)

1中布尔类型:boolean(默认值false)

24.基本类型和包装类型的区别?

  1. 包装类型属于引用数据类型
  2. 包装类型可用于泛型,而基本类型不可以
  3. 相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小
  4. 成员变量包装类型不赋值就是 null ,而基本类型有默认值且不是 null
  5. 对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

25.静态方法为什么不能调用非静态成员?

静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而==非静态成员属于实例对象==,只有在对象实例化之后才存在,需要通过类的实例对象去访问。

在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

26.面向对象和面向过程的区别?

面向对象的优点:易维护,易复用,易扩展

27.如果一个类没有声明构造方法,该程序能正确执行吗?

构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。

如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。

28.构造方法有哪些特点?是否可被 override?

构造方法具有以下特点:

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

29.字符串常量池的作用了解吗?

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

String s1 = new String("abc");这句话创建了几个字符串对象?

答:会创建 1 或 2 个字符串对象。

30.Exception 和 Error 有什么区别?

Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。

ErrorError 属于程序无法处理的错误。例如 Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。

Checked Exception 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 catch或者throws 关键字处理的话,就没办法通过编译。

RuntimeException 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到):

31.Throwable 类常用方法有哪些?

String getMessage(): 返回异常发生时的简要描述

String toString(): 返回异常发生时的详细信息

String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同

void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

32.什么是泛型?有什么作用?

Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。

编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>() 这行代码就指明了该 ArrayList 对象只能传入 Person 对象,如果传入其他类型的对象就会报错。

33.何谓注解?

Annotation (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。

注解本质是一个继承了Annotation 的特殊接口:

JDK 提供了很多内置的注解(比如 @Override@Deprecated),同时,我们还可以自定义注解。

注解只有被解析之后才会生效,常见的解析方法有两种:

34.java的引用传递是怎么样的?

Java 中将实参传递给方法(或函数)的方式是 值传递

java不引入引用传递是因为:

  1. 出于安全考虑,方法内部对值进行的操作,对于调用者都是未知的(把方法定义为接口,调用方不关心具体实现)。你也想象一下,如果拿着银行卡去取钱,取的是 100,扣的是 200,是不是很可怕。
  2. Java 之父 James Gosling 在设计之初就看到了 C、C++ 的许多弊端,所以才想着去设计一门新的语言 Java。在他设计 Java 的时候就遵循了简单易用的原则,摒弃了许多开发者一不留意就会造成问题的“特性”,语言本身的东西少了,开发者要学习的东西也少了。

35.说说 List, Set, Queue, Map 四者的区别?

List: 存储的元素是有序的、可重复的。

Set: 存储的元素不可重复的。

Queue: 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。

Map: 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。

36.ConcurrentHashMap

HashMap 线程不安全主要体现在以下两方面:

  1. 在 JDK 1.7 中的死循环问题
  2. 所有版本中的数据覆盖问题

JDK1.7 的 ConcurrentHashMap

Java7 ConcurrentHashMap 存储结构

JDK1.8 的 ConcurrentHashMap

Java8 ConcurrentHashMap 存储结构

JDK1.8 的 ConcurrentHashMap 不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

37.Exception主要分为受检异常和运行时异常

在 Java 中,异常(Exception)主要分为两大类:

  1. 受检异常(Checked Exception)
  2. 运行时异常(Runtime Exception)

受检异常:

运行时异常

描述: 运行时异常是未受检异常,通常是由编程错误引起的,例如除零错误、空指针引用、数组越界等。这类异常在编译时不强制要求处理,可以选择不捕获或抛出。运行时异常一般表示程序员的逻辑错误,因此往往需要通过调试或重构代码来解决。

常见的例子:

38.面向对象三大特征?

封装 继承 多态

封装:

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

继承:

不同类型的对象,相互之间经常有一定数量的共同点。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

关于继承如下 3 点请记住:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。

多态:

多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

多态的特点:

39.JDK序列化有哪些问题?

  1. 数据体积过大
  2. 有安全漏洞
  3. 可读性差

40.HashMap中的hash()方法为什么要右移16位异或?

如果仅使用 hashCode 的低位来计算数组的索引,容易发生冲突(即不同的键映射到同一个索引)。这种冲突会导致多个键值对被放在同一个桶中,从而退化为链表或红黑树,降低性能。

通过右移16位,将 hashCode高位和低位混合在一起,可以使哈希值更加均匀分布,减少冲突的概率


并发编程

1.进程和线程的区别?

进程和线程的区别也是很明显的,进程是==资源分配的最小单位==,线程是 CPU 调度的最小单位。具体来说:

2.Java 线程和操作系统的线程有啥区别?

JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。

我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下:

顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。

一句话概括 Java 线程和操作系统线程的关系:现在的 Java 线程的本质其实就是操作系统的线程

3.你工作当中是怎么使用线程的?

答:通过线程池来管理线程资源,如果是对外提供服务的接口不使用线程池来创建,有可能在高并发的场景下创建大量的线程从而导致过度的消耗系统和资源,甚至拉垮系统。使用线程池的好处可以减少线程的重复创建与销毁,进而节省系统的资源开销。

4.线程是生命周期和状态?

6种。

NEW: 初始状态,线程被创建出来但没有被调用 start()

RUNNABLE: 运行状态,线程被调用了 start()等待运行的状态。

BLOCKED:阻塞状态,需要等待锁释放。

WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。

TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。

TERMINATED:终止状态,表示该线程已经运行完毕。

Java 线程状态变迁图

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。

TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

当线程进入 synchronized 方法/块或者调用 wait 后(被 notify)重新进入 synchronized 方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。

线程在执行完了 run()方法之后将会进入到 TERMINATED(终止) 状态。

5.什么是线程上下文切换?

线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换

上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。

6.Thread#sleep() 方法和 Object#wait() 方法对比

共同点:两者都可以暂停线程的执行。

区别

7.Synchronized和Lock区别?

可中断锁和不可中断锁有什么区别?

lock的3个方法 lock.lock()上锁 ,lock.unlock()解锁和lock.trylock()尝试获取锁,而不会一直阻塞

Lock默认是非公平锁,我们在构造时参数为true可以设置lock为公平锁

公平锁:非常公平,先来后到排队,后来的线程肯定后执行

非公平锁:即时前面的线程排了很久队,后来了一个线程一样和他们竞争抢锁

Lock用法:

Lock lock = new ReentrantLock();
lock.lock();// 加锁
try {
    // 业务代码
    if (number > 0) {
        System.out.println(Thread.currentThread().getName() + "卖出了" +
                (50 - (--number)) + "张票,剩余:" + number + "张票");
    }
} catch (Exception e) {
    e.printStackTrace();
} finally {
    lock.unlock();// 解锁
}

8.八锁问题

synchronized的锁对象是方法的调用者

class Test1{
    public synchronized void  sendMsg(){
        System.out.println("发短信");
    }
    public synchronized void callPhone(){
        System.out.println("打电话");
    }
}
class Main1{
   Test1 test=new Test1();
    new Thread(()->{
        test.sendMsg();
    },"t1").start();
    new Thread(()->{
		test.callPhone();
    },"t2").start();
}

上面两个方法用的是同一个锁,谁先拿到执行谁,先拿到的没执行完,别的线程调另一方法不会执行,会一直等待

每个对象都有对象头 对象头里有锁的状态 用数字区别

静态方法加synchronized的话锁的就是Class类模板

9.CopyOnWriteArrayList

一些集合类如arrayList在并发下是不安全的,如果我们多线程来向一个list中add元素会抛出ConcurrentModificationException并发修改异常

解决方案:

1.Vector

2.Collections.synchronizedList(new ArrayList<>())

3.CopyOnWriteArrayList

CopyOnWriteArrayList:

写入时复制

CopyOnWriteArrayList比Vector强在哪里?

Vector使用的synchronized关键字,效率低很多,CopyOnWriteArrayList使用的lock锁

CopyOnWriteArrayList适用于读多写少

10.CountDownLatch,CyclicBarrier和Semaphore

CountDownLatch:

import java.util.concurrent.CountDownLatch;
// 计数器
public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {
        // 总数是6,必须要执行任务的时候,再使用!
        CountDownLatch countDownLatch = new CountDownLatch(6);//1.创建countDownLatch初始化
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()
                                                           +" Go out");
                countDownLatch.countDown(); //2.减一操作
            },String.valueOf(i)).start();
        }
        countDownLatch.await(); // 3.等待计数器归零,然后再向下执行
        System.out.println("Close Door");
    }

CyclicBarrier:循环栅栏

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class  CyclicBarrierDemo {
    public static void main(String[] args) {
        /*
         * 集齐7颗龙珠召唤神龙
         */
        // 召唤龙珠的线程
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
            System.out.println("召唤神龙成功!");//等七个线程都执行到cyclicBarrier.await()再执行这句
        });
        for (int i = 1; i <=7 ; i++) {
            final int temp = i;
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
                try {
                    cyclicBarrier.await(); // 等待
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

Semaphore:信号量

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
    public static void main(String[] args) {
        // 线程数量:停车位! 限流!、
        // 如果已有3个线程执行(3个车位已满),则其他线程需要等待‘车位’释放后,才能执行!
        Semaphore semaphore = new Semaphore(3);//信号量为3
        for (int i = 1; i <=6 ; i++) {
            new Thread(()->{
                try {
                    semaphore.acquire();// acquire() 得到
                    System.out.println(Thread.currentThread().getName()+"抢到车位");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println(Thread.currentThread().getName()+"离开车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release(); // release() 释放 
                }
            },String.valueOf(i)).start();
        }
    }
}

11.ReadWriteLock读写锁

使用:

private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();//创建
readWriteLock.writeLock().lock();//写锁
readWriteLock.writeLock().unlock();
readWriteLock.readLock().lock();//读锁
readWriteLock.readLock().unlock();

写锁只能一个一个线程依次执行,读锁可以多个线程同时执行

疑问:那读的时候为什么还要加读锁呢,不加锁不也是多个线程同时读吗?

答:加了读锁就会防止读的时候有写入操作导致幻读

12.阻塞队列

image-20240817154522676
ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue<>(3);

image-20240817155136822

SynchronousQueue 同步队列:

BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); // 同步队列

没有容量,进去一个元素,必须等待取出来之后,才能再往里面放一个元素!

put、take

13.线程池的好处?

  1. 减少资源消耗
  2. 提高响应速度
  3. 更好的控制线程数量:通过线程池,可以限制系统中同时运行的线程数量,防止由于创建过多线程而导致系统资源耗尽或性能下降。
  4. 方便管理和调优:线程池提供了多种配置参数(如核心线程数、最大线程数、队列长度等),使得开发者可以根据应用的特点来调优线程池的性能。
  5. 简化开发:使用线程池可以简化并发编程的难度,开发者只需将任务提交给线程池,无需关心线程的创建、管理和销毁等复杂的操作。

14.并发与并行的区别?

15.为什么要使用多线程?

从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。

从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能

16.单核CPU支持java多线程吗?

单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。

17.线程类型和如何设置线程池最大线程数量?

单核 CPU 同时运行多个线程的效率是否会高,取决于线程的类型和任务的性质。一般来说,有两种类型的线程:

  1. CPU 密集型:CPU 密集型的线程主要进行计算和逻辑处理,需要占用大量的 CPU 资源。
  2. IO 密集型:IO 密集型的线程主要进行输入输出操作,如读写文件、网络通信等,需要等待 IO 设备的响应,而不占用太多的 CPU 资源。

线程池(new ThreadPoolExecutor(7个参数))---最大的线程如何去设置:

io密集型:判断程序中十分耗io的线程数,设置最大线程数大于这个数(通常是两倍)

cpu密集型:cpu几核就设置最大线程为几,可以保持cpu效率最高

Runtime.getRuntime().availableProcessors()//获取cpu核数

18.什么是线程死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

死锁的四个必要条件:

  1. 互斥条件(Mutual Exclusion):资源只能由一个线程(或进程)独占使用。即某资源在某一时刻只能被一个线程占有,如果有其他线程请求该资源,则该线程必须等待。
  2. 占有并等待(Hold and Wait):一个线程已经占有了至少一个资源,同时又请求新的资源,但该资源当前被其他线程占用,所以该线程进入等待状态。
  3. 不可剥夺(No Preemption):线程已经占有的资源在未使用完之前,不能被强行剥夺,只能在使用完毕后由线程自行释放。
  4. 循环等待(Circular Wait):存在一个线程的集合,每个线程都在等待下一个线程所占有的资源,形成一个循环等待链。例如,线程A等待线程B所占有的资源,线程B等待线程C所占有的资源,而线程C又在等待线程A所占有的资源。

破坏四个条件中的任意一个都能破解死锁

如何发现死锁?

1、使用jsp -l定位进程号

2、使用jstack 进程号找到死锁问题

19.线程池:3大方法,7大参数,4种拒绝策略

3大方法:

Executors.newSingleThreadExecutor();// 创建单个线程的线程池
Executors.newFixedThreadPool(5);// 创建一个固定大小的线程池
Executors.newCachedThreadPool();// 创建一个可伸缩的线程池

☆但是建议使用底层方法==new ThreadPoolExecutor(7个参数)==来创建线程池,防止oom

在Java中,使用Executors类提供的静态方法创建线程池时,可能会导致OOM(OutOfMemoryError)的问题,主要是由于其默认的线程池配置可能不够灵活,导致资源管理不当。以下是一些可能导致OOM的原因:

  1. 固定大小的线程池: Executors类提供的一些静态方法,例如Executors.newFixedThreadPool()创建的线程池是固定大小的,如果提交的任务量超过了线程池的容量,那么任务会被放入无界队列中,导致内存消耗过多。如果持续提交任务,队列可能会无限增长,最终导致内存耗尽。
  2. 无界队列: Executors类创建的线程池通常使用无界队列,例如LinkedBlockingQueue,这意味着队列可以无限增长。如果生产者速度快于消费者速度,队列会持续增长,最终导致内存耗尽。
  3. 缓存型线程池: Executors.newCachedThreadPool()创建的线程池是一个可缓存的线程池,它会根据需求动态调整线程数量。但是,如果任务提交速度过快,导致线程数持续增长,最终可能导致内存耗尽。

相比之下,使用ThreadPoolExecutor类直接创建线程池能够提供更灵活的配置选项,例如指定核心线程数、最大线程数、队列类型以及拒绝策略等。通过合理地配置这些参数,可以更好地控制线程池的行为,避免OOM问题。

7大参数:

public ThreadPoolExecutor(int corePoolSize, // 核心线程池大小
                          int maximumPoolSize, // 最大核心线程池大小
                          long keepAliveTime, // 超时没有人调用就会释放
                          TimeUnit unit, // 超时单位
                          BlockingQueue<Runnable> workQueue,// 阻塞队列
                          ThreadFactory threadFactory,// 线程工厂:创建线程的,一般 不用动
                          RejectedExecutionHandler handle )// 拒绝策略

举例:

//手动创建一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
        5,
        3,
        TimeUnit.MINUTES,
        new LinkedBlockingDeque<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());//七个参数
}

4种拒绝策略:

new ThreadPoolExecutor.AbortPolicy() 
队列满了,还有任务进来,不处理该任务,抛出异常

new ThreadPoolExecutor.CallerRunsPolicy() 
哪来的去哪,比如是main来的任务,队列满了,交回给让main处理

new ThreadPoolExecutor.DiscardPolicy() 
队列满了,丢掉任务,不会抛出异常!

new ThreadPoolExecutor.DiscardOldestPolicy() 
队列满了,尝试去和最早的竞争,也不会抛出异常!

20.异步回调CompletableFuture

没有返回值的 runAsync 异步回调

// 没有返回值的 runAsync 异步回调
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(
        Thread.currentThread().getName()+"runAsync=>Void");
});

System.out.println("1111");

completableFuture.get(); // 获取阻塞执行结果

有返回值的 supplyAsync 异步回调

completableFuture.whenComplete()获取成功的返回结果

completableFuture.exceptionally()获取到错误的返回结果

// 有返回值的 supplyAsync 异步回调
// ajax,成功和失败的回调
// 返回的是错误信息;
CompletableFuture<Integer> completableFuture = 
                        CompletableFuture.supplyAsync(()->{
    System.out.println(Thread.currentThread().getName()
                                   +"supplyAsync=>Integer");
    int i = 10/0;
    return 1024;
});
System.out.println(completableFuture.whenComplete((t, u) -> {
    System.out.println("t=>" + t); // 正常的返回结果
    System.out.println("u=>" + u); 
    // 错误信息:
    // java.util.concurrent.CompletionException: 
    // java.lang.ArithmeticException: / by zero
}).exceptionally((e) -> {
    System.out.println(e.getMessage());
    return 233; // 可以获取到错误的返回结果
}).get());

21.JMM java内存模型

JMM是个概念,是不存在的东西,是一种约定

屏蔽各种硬件或系统内存的访问差异,实现java程序在各平台达到一致的内存访问效果

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回主存。

2、线程加锁前,必须读取主存中的最新值到工作内存中!

3、加锁和解锁是同一把锁。

线程 工作内存主内存

8种操作:

22.volatile

  1. 保证可见性
  2. 不保证原子性
  3. 禁止指令重排

1.可见性:如果我们将变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。

2.不保证原子性:虽然 volatile 变量保证了对它的操作是立即可见的,但它并不能保证对该变量的操作是原子的。如果一个变量的操作需要保证原子性,通常需要使用 synchronized 关键字(或者lock锁)或者 java.util.concurrent.atomic 包中的原子类

3.禁止指令重排:

什么是指令重排?答:我们写的程序,计算机并不是按照你写的那样去执行的,源代码 —> 编译器优化的重排 —> 指令并行也可能会重排 —> 内存系统也会重排 ——> 执行

volatile 可以避免指令重排:

内存屏障。CPU指令。作用:

  1. 保证特定操作的执行顺序!
  2. 可以保证某些变量的内存可见性 (利用这些特性volatile 实现了可见性)

所以本题答案可回答:volatile 是可以保证可见性。不能保证原子性,由于内存屏障,可以保证避免指令重排的现象产生!

volatile 关键字在底层的实现主要是通过内存屏障(memory barrier)来实现的。内存屏障是一种 CPU 指令,用于强制执行 CPU 的内部缓存与主内存之间的数据同步。

在 Java 中,当线程读取一个 volatile 变量时,会从主内存中读取变量的最新值,并把它存储到线程的工作内存中。当线程写入一个 volatile 变量时,会把变量的值写入到线程的工作内存中,并强制将这个值刷新到主内存中。这样就保证了 volatile 变量的可见性和有序性。

23.CAS和ABA问题

CAS(Compare and Swap 比较和交换)是一种用于实现多线程同步的原子操作。它是一种乐观锁的实现方式,在并发编程中广泛应用。

实现 CAS(Compare-And-Swap, 比较并交换)操作的一个关键类是Unsafe

CAS是CPU的并发原语!java无法操作内存,但是java可以调用c++让c++操作内存 unsafe类就是java的后门,可以通过unsafe类操作内存

自带原子性

缺点:1.循环会耗时 2.一次性只能保证一个共享变量的原子性 3.会存在ABA问题

尽管自旋会导致CPU循环等待,但它不会导致线程进入阻塞状态,而是占用CPU资源进行忙等待

CAS 操作仅能对单个共享变量有效。当需要操作多个共享变量时,CAS 就显得无能为力。不过,从 JDK 1.5 开始,Java 提供了AtomicReference(原子引用)类,这使得我们能够保证引用对象之间的原子性。通过将多个变量封装在一个对象中,我们可以使用AtomicReference来执行 CAS 操作。

除了 AtomicReference 这种方式之外,还可以利用加锁来保证。

ABA问题(狸猫换太子)

ABA 问题是在使用 CAS(Compare and Swap)操作时可能遇到的一种情况,它主要涉及到原子操作的可见性和一致性问题。

具体来说,ABA 问题指的是在一个线程读取一个变量的值 A,然后另一个线程修改该变量的值为 B,再次修改回 A,此时第一个线程再次读取该变量的值时,由于值仍然是 A,所以它会错误地认为该变量的值并未被修改过。这样一来,尽管变量的值发生了变化,但是由于在修改前后的值都是 A,因此 CAS 操作可能会误认为没有其他线程修改过该变量。

ABA 问题可能导致一些意外的行为和数据不一致性。例如,在使用 CAS 进行某种状态判断时,可能会出现误判,从而导致程序的行为不符合预期。这对于需要保证数据的一致性和正确性的场景来说是一个潜在的风险。

为了解决 ABA 问题,可以使用版本号或者时间戳等机制来确保 CAS 操作的原子性和一致性。通过引入一个额外的变量,记录每次变量的修改次数或者修改时间戳,可以在进行 CAS 操作时检查这个变量,从而避免 ABA 问题。

在 Java 中,引入==原子引用==,AtomicStampedReference 类提供了一种解决 ABA 问题的方案,它可以在 CAS 操作时同时比较引用和版本号,从而避免了 ABA 问题的发生。

24.如何预防和避免线程死锁?

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件:一次性申请所有的资源。
  2. 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

25.DCL单例模式

public class Singleton {

    private volatile static Singleton uniqueInstance;//volatile防止指令重排

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

volatile防止指令重排:

因为对于uniqueInstance = new Singleton();//这句代码可以分为3步
/*
1.申请内存空间
2.创建对象
3.将内存空间指向对象
正常顺序是123 但是指令重排可能会变为132,这样会导致第一个线程执行了13没执行2,这时第二个线程进来认为uniqueInstance != null直接返回该对象了,但这时该对象还没创建,是一片虚无
*/

26.乐观锁和悲观锁

​ 1.悲观锁:

悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。

​ 2.乐观锁:

乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。

在 Java 中java.util.concurrent.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

各自比较:

悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如LongAdder),也是可以考虑使用乐观锁的,要视实际情况而定。

乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考java.util.concurrent.atomic包下面的原子变量类)。

27.synchronized 锁升级

锁主要存在四种状态,依次是:无锁状态偏向锁状态轻量级锁状态重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

image-20240818194002111

1.无锁状态 当前无任何线程获取它,所以此过程不需要加锁,是无锁状态。当一个线程访问一个同步块时,如果该同步块没有被其他线程占用,那么该线程就可以直接进入同步块,并且将同步块标记为偏向锁状态。

2.偏向锁状态 在偏向锁状态下,同步块已经被一个线程占用,其他线程访问该同步块时,只需要判断该同步块是否被当前线程占用,如果是,则直接进入同步块。这个过程不需要进行任何加锁操作,仍然属于乐观锁状态。

3.轻量级锁状态 如果在偏向锁状态下,有多个线程竞争同一个同步块,那么该同步块就会升级为轻量级锁状态。此时,每个线程都会在自己的 CPU 缓存中保存该同步块的副本,并通过 CAS(Compare and Swap)操作来对同步块进行加锁和解锁。这个过程需要进行加锁操作,但相对于传统的 mutex 锁,轻量级锁的效率要高很多。

4.重量级锁状态 轻量级锁之后会通过自旋来获取锁,自旋执行一定次数之后还未成功获取到锁,此时就会升级为重量级锁,并且进入阻塞状态。

image-20240818194529166

28.synchronized 和 volatile 有什么区别?

29.AQS(抽象队列同步器)

是一个抽象类,主要用来构建锁和同步器(提供了线程同步的底层实现机制

AQS为构建锁和同步器提供了一些通用功能的实现

AQS思想:

如果被请求的共享资源空闲,便将当前线程设为有效线程并将共享资源锁定。

如果共享资源被占用,就需要一套线程阻塞等待和被唤醒时锁分配机制,该机制AQS是基于CLH锁(是对自旋锁的改进,虚拟双向队列)实现的

基于AQS的常见工具类:

AQS提供了两种锁共享锁和排他锁

Lock中的ReetrantLock用到了排他锁功能

Semaphore 和CountDownLatch 用到了共享锁

用互斥变量state记录锁竞争的状态,0为无锁,1为有锁。state 变量由 volatile 修饰,用于展示当前临界资源的获锁情况。

未获得锁的线程会阻塞到一个双向链表,遵循FIFO原则

以可重入的互斥锁 ReentrantLock 为例,它的内部维护了一个 state 变量,用来表示锁的占用状态。state 的初始值为 0,表示锁处于未锁定状态。当线程 A 调用 lock() 方法时,会尝试通过 tryAcquire() 方法独占该锁,并让 state 的值加 1。如果成功了,那么线程 A 就获取到了锁。如果失败了,那么线程 A 就会被加入到一个等待队列(CLH 队列)中,直到其他线程释放该锁。假设线程 A 获取锁成功了,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加)。
这就是可重入性的体现:一个线程可以多次获取同一个锁而不会被阻塞。但是,这也意味着,一个线程必须释放与获取的次数相同的锁,才能让 state 的值回到 0,也就是让锁恢复到未锁定状态。只有这样,其他等待的线程才能有机会获取该锁。

30.构造方法可以用synchronized修饰吗?

不能。不过可以在构造方法内部使用synchronized代码块

构造方法本身是线程安全的,但如果在构造方法中涉及共享资源的操作,就需要采取适当的措施来保证整个构造过程的线程安全。

31.synchronized底层原理了解吗?

synchronized底层原理属于JVM层面的东西

查看字节码文件发现:

32.可重入锁是什么?

可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。

JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字锁都是可重入的。

33.读锁为什么不能升级为写锁?

写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。

另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。

34.ThreadLocal是什么?底层原理?

通过ThreadLocal可以使每一个线程都有自己的专属本地变量

原理:

每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。

image-20240818154420087

35.ThreadLocal 内存泄露问题是怎么导致的?

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法

36.池化技术的思想?

池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。

37.线程池的核心线程会被回收吗?

ThreadPoolExecutor 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 allowCoreThreadTimeOut(boolean value) 方法的参数设置为 true,这样就会回收空闲(时间间隔由 keepAliveTime 指定)的核心线程了。

38.什么是线程通讯?如何通讯?

线程通讯指的是多个线程之间通过共享内存消息传递等方式来协调和同步它们的执行。

如何通讯:

实现方法:

1.wait() 和 notify()

2.Semaphore 类

3.CyclicBarrier 类

4.Lock 接口和 Condition 接口来实现

39.解决死锁

死锁的常用解决方案有以下两个:

  1. 按照顺序加锁:尝试让所有线程按照同一顺序获取锁,从而避免死锁。
  2. 设置获取锁的超时时间:尝试获取锁的线程在规定时间内没有获取到锁,就放弃获取锁,避免因为长时间等待锁而引起的死锁。

40.生产者消费者模型主要是用来解决什么问题的?

主要用来解决==多线程环境下的协调与同步问题==

41.生产者消费者代码简单编写

public class Duoxiancheng {

    public static void main(String[] args) {
        Mark mark = new Mark();
        //消费者线程
        new Thread(()->{
            int i=0;
            while(i<20){
                try {
                    mark.add();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                i++;
            }
        }).start();
        
        //生产者线程
        new Thread(()->{
            int j=0;
            while (j<20){
                try {
                    mark.remove();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                j++;
            }
        }).start();
    }


}

class Mark{
    int data=0;

    public synchronized void add() throws InterruptedException {
        while(data!=0){
            this.wait();
        }
        data++;
        System.out.println("消费者消费了,现在为0");
        this.notifyAll();
    }

    public synchronized void remove() throws InterruptedException {
        while(data==0){
            this.wait();
        }
        data--;
        System.out.println("生产者生产了,现在为1");
        this.notifyAll();
    }
}

Spring

1.spring是什么?

Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。

我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,Spring 提供的核心功能主要是 IoC 和 AOP

Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。

spring的核心特点:

  1. 轻量级
  2. 控制反转(IOC)
  3. 面向切面编程(AOP)
  4. 声明式事务管理
  5. 框架整合
  6. 测试支持

2.什么是IOC?

ioc(inversion of control)控制反转,是spring框架的核心概念之一

是一种设计思想而不是一种具体的技术实现,ioc思想就是将原本在程序中手动创建对象的控制权,交由spring框架来管理。

在传统编程模式下,对象之间的创建、组装和管理都是由开发人员手动完成的,而在ioc模式下,这些责任都委托给了ioc容器来管理。

在ioc模式中,本该由开发人员手动控制对象的依赖关系变为了由容器自动注入。这样反转的控制1.使得各模块解耦 2.提高代码灵活性、可维护性和可测试性。

ioc的实现依赖于ioc容器这个组件。ioc容器负责创建和管理对象,以及解决对象之间的依赖关系。

IoC 容器通过以下两种主要的方式来实现控制反转:

  1. 依赖注入(Dependency Injection,DI):依赖注入是 IoC 的一种具体实现方式,通过将依赖关系注入到对象中,实现了对象之间的解耦。容器负责查找依赖对象,并将其自动注入到相应的对象中。依赖注入可以通过构造函数、Setter 方法或接口注入来完成
  2. 依赖查找(Dependency Lookup):依赖查找是另一种 IoC 的实现方式,它通过容器提供的 API,开发人员手动查找和获取所需的依赖对象。开发人员在代码中通过容器提供的接口来获取所需的对象实例,从而实现了对象之间的解耦。

ioc的优点?

总之ioc是Spring框架的基石,是Spring框架众多特性的基础。

3.什么是AOP?

AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理日志管理权限控制缓存控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

AOP 的实现依赖于以下几个概念:

优点:

  1. 模块化
  2. 减少重复代码
  3. 解耦

AOP的实现依靠动态代理技术(JDK 动态代理和 CGLIB动态代理):

对于基于接口的目标类,Spring 使用 JDK 动态代理;

对于没有实现接口的目标类,Spring 使用 CGLIB 生成子类来实现代理。

SpringAOP和AspectJ:

SpringAOP和AspectJ是AOP面向切面编程的两种实现方式,两者并没有特别强的关系。

AspectJ是基于编译器实现的,当一个类编译成字节码文件的时候,会将定义的一些额外逻辑织入字节码文件中;

SpringAOP是基于动态代理机制实现的,使用动态代理生成代理对象后,当执行某些方法时,会执行一些额外定义的切面逻辑。

image-20240818225018279

4.依赖注入和依赖查找的区别?

依赖注入和依赖查找的区别在于,依赖注入是将依赖关系委托给容器,由容器来管理对象之间的依赖关系;而依赖查找是由对象自己来查找它所依赖的对象,容器只负责管理对象的生命周期。

5.依赖注入方式有哪些?

  1. 构造器注入
  2. setter方法注入
  3. 字段注入
  4. 接口/工厂方法注入

6.什么是Bean?Bean有哪几种配置方式?

Bean 代指的就是那些被 IoC 容器所管理的对象。

我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。

Bean有哪几种配置方式?

  1. XML配置使用XML文件来配置Bean,通过元素定义Bean的属性和依赖关系。可以使用Spring的XML命名空间和标签来简化配置。

  2. 注解配置:使用注解来配置Bean,通过在Bean类上添加注解,如@Component、@Service、@Repository等,来标识Bean的角色和作用

  3. JavaConfig方式:使用Java类来配置Bean,通过编写一个配置类,使用@Configuration注解标识,然后在方法上使用@Bean注解来定义Bean

  4. @Import:@Import注解可以用于导入其他配置类,也可以用于导入其他普通类。当导入的是配置类时,被导入的配置类中定义的Bean会被纳入到当前配置类的上下文中;当导入的是普通类时,被导入的类本身会被当作一个Bean进行注册。

    这4种是最常用的,另外还有两种一些冷门的:

  5. Groovy配置:使用Groovy脚本来配置Bean,通过编写一个Groovy脚本文件,使用Spring的DSL(Domain Specific Language)来定义Bean。

  6. JSR-330:Spring提供了对JSR-330标准注释(依赖注入)的支持,可以使用@Named 或 @ManagedBean替代@Component,但是需要javax.inject依赖。

7.将一个类声明为 Bean 的注解有哪些?

@Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用@Component 注解标注。

@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。

@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。

@Controller : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。

8.@Component 和 @Bean 的区别是什么?

9.注入 Bean 的注解有哪些?

Spring 内置的 @Autowired 以及 JDK 内置的 @Resource@Inject 都可以用于注入 Bean。

image-20240818205553261

10.@Autowired 和 @Resource 的区别是什么?

@Autowired:

Autowired 属于 Spring 内置的注解,

默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。

这个注解中,有一个required属性,默认值为true,表示必须实例化一个注入的Bean。如果找不到对应类型的Bean,会在应用启动时报错。

存在的问题: 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。

这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。

@Resource:

@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType

@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)

总结:

@Autowired默认根据接口类型匹配注入,如果有多个实现类则变为根据名称匹配(首字母小写)

@Resource默认根据名称匹配注入,如果根据名称找不到则变为根据类型匹配,还可以手动指定属性name和type

11.Bean 的作用域有哪些?

如何配置 bean 的作用域呢?

xml方式:

<bean id="..." class="..." scope="singleton"></bean>

@Scope注解方式:

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

12.Bean 是线程安全的吗?

取决于Bean的作用域和状态。

prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。

singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。

不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。

对于有状态单例 Bean 的线程安全问题,常见的解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量
  2. 在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
  3. 加锁(synchronized)

13.Bean 的生命周期了解么?

  1. 实例化:在Java应用程序中,Bean对象是通过new关键字或者反射机制来实例化的。在这个阶段,Bean对象被创建,并分配了内存空间。
  2. 设置属性:(Bean注入和装配)
  3. 初始化:当Bean对象被创建后,需要进行初始化,包括设置属性值、执行一些初始化操作等。在Spring框架中,Bean的初始化可以通过配置文件中的init-method属性进行指定。
  4. 使用:在Bean初始化之后,它就可以被应用程序使用了。在使用过程中,Bean可能会调用其他对象的方法,从而导致其他Bean对象被实例化和初始化。
  5. 销毁:当Bean对象不再被使用时,应该将其销毁并释放占用的内存空间。在Spring框架中,Bean的销毁可以通过配置文件中的destroy-method属性进行指定。

14.AOP五种通知类型和执行顺序?

image-20240818225215702

执行顺序:

image-20240818224337245

15.spring IOC容器启动流程是什么?

  1. 加载配置文件:Spring IOC容器会读取配置文件,其中可能包含XML配置、Java配置或者注解配置。
  2. 解析配置:容器解析配置文件,识别出其中定义的各个Bean以及它们之间的依赖关系。
  3. 实例化Bean:容器根据配置信息,实例化各个Bean对象。
  4. 依赖注入:容器将实例化后的Bean注入到它们所依赖的其他Bean中。
  5. 初始化Bean:对于实现了InitializingBean接口或者在配置文件中配置了初始化方法的Bean,容器会调用其初始化方法进行一些额外的初始化工作。
  6. 完成容器初始化:容器初始化完成后,可以从容器中获取需要的Bean进行后续操作。

16.说一下spring 的事务隔离级别?

  1. DEFAULT(默认):使用数据库默认的事务隔离级别。通常为数据库的默认隔离级别,如Oracle为READ COMMITTED,MySQL为REPEATABLE READ。
  2. READ_UNCOMMITTED(读未提交):最低的隔离级别,允许读取未提交的数据。事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读的问题。
  3. READ_COMMITTED(读已提交):保证一个事务只能读取到已提交的数据。事务读取的数据是其他事务已经提交的数据,避免了脏读的问题。但可能会出现不可重复读和幻读的问题。
  4. REPEATABLE_READ(可重复读):保证一个事务在同一个查询中多次读取的数据是一致的。事务期间,其他事务对数据的修改不可见,避免了脏读和不可重复读的问题。但可能会出现幻读的问题。
  5. SERIALIZABLE(串行化):最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读的问题。但会降低并发性能,因为事务需要串行执行。image-20240818231147751

17.说一下Spring的事务传播行为?

  1. REQUIRED:(required)如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。这是最常用的传播行为,也是默认的,适用于大多数情况。
  2. REQUIRES_NEW:(requires_new)无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于需要独立事务执行的场景,不受外部事务的影响。
  3. SUPPORTS:(supports)如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。适用于不需要强制事务的场景,可以与其他事务方法共享事务。
  4. NOT_SUPPORTED:(not_supported)以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,可以在方法执行期间暂时禁用事务。
  5. MANDATORY:(mandatory)如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。适用于必须在事务中执行的场景,如果没有事务则会抛出异常。
  6. NESTED:(nested)如果当前存在事务,则在嵌套事务中执行,如果当前没有事务,则创建一个新的事务。嵌套事务是外部事务的一部分,可以独立提交或回滚。适用于需要在嵌套事务中执行的场景。
  7. NEVER:(never)以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则会抛出异常。

18.谈谈你对spring的理解?

首先Spring是一个生态:可以构建企业级应用程序所需的一切基础设施

通常Spring指的就是Spring Framework,它有两大核心:

1.IOC 和 DI 的支持

Spring 的核心就是一个大的工厂容器,可以维护所有对象的创建和依赖关系,Spring 工厂用于生成 Bean,并且管理 Bean 的生命周期,实现高内聚低耦合的设计理念。

2.AOP 编程的支持

Spring 提供了面向切面编程,面向切面编程允许我们将横切关注点从核心业务逻辑中分离出来,实现代码的模块化和重用。可以方便的实现对程序进行权限拦截、运行监控、日志记录等切面功能。

总结一句话:它是一个轻量级、非入侵式的控制反转 (IoC) 和面向切面 (AOP) 的容器框架。

19.MyBatis一、二级缓存和Spring一二级缓存有什么关系?

答:完全没有关系。

20.JDK 动态代理和 CGLIB 动态代理区别?

  1. JDK 动态代理基于接口,要求目标对象实现接口;CGLIB 动态代理基于类,可以代理没有实现接口的目标对象。
  2. JDK 动态代理使用 java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler 来生成代理对象;CGLIB 动态代理使用 CGLIB 库来生成代理对象。
  3. JDK 动态代理生成的代理对象是目标对象的接口实现;CGLIB 动态代理生成的代理对象是目标对象的子类。
  4. JDK 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。
  5. CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类。

21.BeanFactory 和 FactoryBean有什么区别?

BeanFactory :

BeanFactory 是 Spring 框架的基本容器,负责管理和创建应用程序中的对象。BeanFactory 的主要职责是实例化 Bean、处理 Bean 之间的依赖关系、注入属性以及在需要时销毁 Bean。

// 得到 BeanFactory 对象
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("spring-config.xml"));
// 使用 BeanFactory 得到 User 对象
beanFactory.getBean("user");

FactoryBean:

FactoryBean 是一个特殊的 Bean,它实现了 Spring 的 FactoryBean 接口。

它是一种更加灵活和可扩展的机制,可以通过编程的方式动态地创建和配置Bean。

FactoryBean 的实现类可以自定义创建和管理 Bean 的逻辑。例如,它可以根据条件选择性地创建不同的实例,或者在创建 Bean 之前进行一些初始化操作。FactoryBean 通常在 Spring 配置文件中配置,并由 BeanFactory 负责实例化和管理。

import org.springframework.beans.factory.FactoryBean;

public class MyBeanFactory implements FactoryBean<MyBean> {

    @Override
    public MyBean getObject() throws Exception {
        // 在这里定义创建 Bean 的逻辑
        MyBean myBean = new MyBean();
        // 进行一些初始化操作
        myBean.setName("Example Bean");
        // 返回创建的 Bean 实例
        return myBean;
    }

    @Override
    public Class<?> getObjectType() {
        // 返回创建的 Bean 的类型
        return MyBean.class;
    }

    @Override
    public boolean isSingleton() {
        // 指示创建的 Bean 是否是单例
        return true;
    }
}

FactoryBean接口定义了两个方法:getObject()和getObjectType()
getObjectType()方法用于返回创建的Bean对象的类型。
getObject()方法用于返回创建的Bean对象,最终该Bean对象会进行注入

总结:

BeanFactory 是 Spring 的基本容器,用于创建和管理 Bean 实例,而 FactoryBean 是一个特殊的 Bean,用于创建其他 Bean 实例,并提供一些初始化 Bean 的设置。

22.Spring事务失效的场景有哪些?

1.方法是private也会失效,解决:改成public: Spring的事务代理通常是通过Java动态代理或CGLIB动态代理生成的,这些代理要求目标方法是公开可访问的(public)。私有方法无法被代理,因此事务将无效。解决方法是将目标方法改为public或protected。

2.目标类没有配置为Bean也会失效,解决:配置为Bean: Spring的事务管理需要在Spring容器中配置的Bean上才能生效。如果目标类没有被配置为Spring Bean,那么事务将无法被应用。解决方法是确保目标类被正确配置为Spring Bean。

3.自己捕获了异常,解决:不要捕获处理: Spring事务管理通常依赖于抛出未捕获的运行时异常来触发事务回滚。如果您在方法内部捕获了异常并处理了它,事务将不会回滚。解决方法是让异常在方法内部被抛出,以触发事务回滚。

4.同一个类中方法调用,导致 @Transactional 失效,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这是由于使用 Spring AOP 代理造成的,因为 只有当事务方法被 当前类以外的代码 调用时,才会由Spring生成的代理对象来管理。

5.@Transactional 注解属性 rollbackFor 设置错误,Spring默认抛出了未检查unchecked异常(继承自 RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor 属性,如果未指定 rollbackFor 属性则事务不会回滚。

6.事务的传播行为设置错误,@Transactional 注解属性 propagation设置错误

若是错误的配置以下三种 propagation,事务将不会发生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。

TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

23.Spring多线程事务 能否保证事务的一致性?

在多线程环境下,Spring事务管理默认情况下无法保证全局事务的一致性。这是因为Spring的本地事务管理是基于线程的,每个线程都有自己的独立事务。

Spring的事务管理通常将事务信息存储在ThreadLocal中,这意味着每个线程只能拥有一个事务。这确保了在单个线程内的数据库操作处于同一个事务中,保证了原子性。

可以通过如下方案进行解决:

24.什么情况下AOP会失效,怎么解决?

1.非Spring管理的对象

Spring的AOP只能拦截由Spring容器管理的Bean对象。如果您使用了非受Spring管理的对象,则AOP将无法对其进行拦截。

2.同一个Bean内部方法调用

如果一个Bean内部的方法直接调用同一个Bean内部的另一个方法,AOP将无法拦截这个内部方法调用。因为AOP是基于代理的,只有通过代理对象才能触发AOP拦截。

3.静态方法

Spring的AOP只能拦截非静态方法。如果您尝试拦截静态方法,AOP将无法生效。

4.final方法

AOP无法拦截final方法。final方法是不可重写的,因此AOP无法生成代理对象来拦截这些方法。
直接在对象内部调用方法:如果您直接在对象内部调用方法而不通过代理对象,AOP将无法拦截。因此,建议始终通过代理对象调用方法以确保AOP的生效。

5.使用private修饰方法

动态代理失效

25.BeanFactory 和 ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。其中ApplicationContext是BeanFactory的子接口

  1. BeanFactory:是Spring框架的核心接口之一, 我们可以称之为 “低级容器”。为什么叫低级容器呢?因为Bean的生产过程分为【配置的解析】和【Bean的创建】,而BeanFactory只有Bean的创建功能,但也说明它内存占用更小,在早期会在一些内存受限的可穿戴设备中作为spring容器使用。

  2. ApplicationContext 可以称之为 “高级容器”。因为他比 BeanFactory 多了更多的功能。他继承了多个接口,因此具备了更多的功能。例如配置的读取、解析、扫描等,还加入了 如 事件事件监听机制,及后置处理器让Spring提升了扩展性。所以你看他的名字,已经不是 BeanFactory 之类的工厂了,而是 “应用上下文”, 代表着整个大容器的所有功能。该接口定义了一个 refresh 方法,此方法是所有阅读 Spring 源码的人的最熟悉的方法,用于刷新整个容器,即重新加载/刷新所有的 bean。

26.SpringMVC的拦截器和过滤器有什么区别?执行顺序?

  1. 归属不同拦截器是SpringMVC框架的一部分,而过滤器是Servlet规范的一部分。
  2. 执行顺序不同:一般来说,首先执行过滤器,然后再执行拦截器。
  3. 用途不同:拦截器主要用于对控制器层的请求进行处理,它们提供了更细粒度的控制,可以在请求进入控制器之前和之后执行特定的逻辑,例如身份验证、日志记录和权限检查。过滤器独立于SpringMVC,用于处理通用的请求和响应内容,例如字符编码、压缩和安全性。

其他说法:

  1. 在作用域方面,Filter作用域每个请求,而interceptor主要就是对Controller方法的请求起作用。
  2. 在执行时机方面,filter会在请求加入dispatchServlet前和返回给客户端前执行,而interceptor会在执行Controller方法的时候和进行视图渲染前执行。
  3. Filter会先执行而interceptor随后执行。

27.Spring和SpringMVC为什么需要父子容器?

1,可以清晰的定义每个容器的职责
2,限制组件之间的依赖关系
3,子容器可以访问父容器中的组件
4,父容器的Bean可以在整个应用程序范围内共享
5,有助于管理和组织应用程序的各个部分

28.Spring 框架中都用到了哪些设计模式?

​ 1.简单工厂
BeanFactory:Spring的BeanFactory充当工厂,负责根据配置信息创建Bean实例。它是一种工厂模式的应用,根据指定的类名或ID创建Bean对象。
​ 2.工厂方法
FactoryBean:FactoryBean接口允许用户自定义Bean的创建逻辑,实现了工厂方法模式。开发人员可以使用FactoryBean来创建复杂的Bean实例。
​ 3.单例模式
Bean实例:Spring默认将Bean配置为单例,确保在容器中只有一个共享的实例,这有助于节省资源和提高性能。
​ 4.适配器模式
SpringMVC中的HandlerAdapter:SpringMVC的HandlerAdapter允许不同类型的处理器适配到处理器接口,以实现统一的处理器调用。这是适配器模式的应用。 5.装饰器模式
BeanWrapper:Spring的BeanWrapper允许在不修改原始Bean类的情况下添加额外的功能,这是装饰器模式的实际应用。
​ 6.代理模式
AOP底层:Spring的AOP(面向切面编程)底层通过代理模式来实现切面功能,包括JDK动态代理和CGLIB代理。
​ 7.观察者模式
Spring的事件监听:Spring的事件监听机制是观察者模式的应用,它允许组件监听和响应特定类型的事件,实现了松耦合的组件通信。
​ 8.策略模式
excludeFilters、includeFilters:Spring允许使用策略模式来定义包扫描时的过滤策略,如在@ComponentScan注解中使用的excludeFilters和includeFilters。
​ 9.模板方法模式
Spring几乎所有的外接扩展:Spring框架的许多模块和外部扩展都采用模板方法模式,例如JdbcTemplate、HibernateTemplate等。
​ 10.责任链模式
AOP的方法调用:Spring AOP通过责任链模式实现通知(Advice)的调用,确保通知按顺序执行。

29.Spring事件监听的核心机制是什么?

观察者模式: 它允许一个对象(称为主题或被观察者)维护一组依赖于它的对象(称为观察者),并在主题状态发生变化时通知观察者。
它包含三个核心:
1.事件: 事件是观察者模式中的主题状态变化的具体表示,它封装了事件发生时的信息。在Spring中,事件通常是普通的Java对象,用于传递数据或上下文信息。
2.事件发布者: 在Spring中,事件发布者充当主题的角色,负责触发并发布事件。它通常实现了ApplicationEventPublisher接口或使用注解@Autowired来获得事件发布功能。
3.事件监听器: 事件监听器充当观察者的角色,负责监听并响应事件的发生。它实现了ApplicationListener接口,通过onApplicationEvent()方法来处理事件。
总之,Spring事件监听机制的核心机制是观察者模式,通过事件、事件发布者和事件监听器的协作,实现了松耦合的组件通信,使得应用程序更加灵活和可维护。

30.为什么SpringBoot的jar可以直接运行?

因为它是一个 可执行的 jar 文件,即包含了一个内嵌的 Web 服务器和所有应用运行所需的依赖。

Tomcat 为 Spring Boot 框架默认的 Web 容器。

31.SpringBoot同时可以处理多少请求?

默认情况下 Spring Boot 能够同时处理的请求数=最大连接数(8192)+最大等待数(100),结果为 8292 个。

当然,这两个值是可以在 Spring Boot 配置文件中修改的,如下配置所示:

server:
  tomcat:
    max-connections: 2000 # 最大连接数
    accept-count: 200 # 最大等待数

32.加入事务和嵌套事务有什么区别?

Propagation.REQUIRED:表示如果当前存在事务,则在当前事务中执行;如果当前没有事务,则创建一个新的事务并在其中执行。即,方法被调用时会尝试加入当前的事务,如果不存在事务,则创建一个新的事务。如果外部事务回滚,那么内部事务也会被回滚。

Propagation.NESTED:表示如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则创建一个新的事务并在其中执行。嵌套事务是独立于外部事务的子事务,它具有自己的保存点,并且可以独立于外部事务进行回滚。如果嵌套事务发生异常并回滚,它将会回滚到自己的保存点,而不影响外部事务。

33.谈谈你对SpringBoot的理解?

1.内置Starter和自动配置:Spring Boot提供了丰富的内置Starter,这些Starter是预定义的依赖集合,可以轻松集成各种主流框架和技术。同时,Spring Boot通过自动配置大大减少了繁琐的配置工作,让开发人员可以直接开箱即用

2.零XML配置:Spring Boot采用JavaConfig的方式进行开发,不需要编写大量的XML配置文件。这种零XML的开发方式让开发更加简洁和可读,同时提高了可维护性。

3.内置Web容器:Spring Boot内置了多个Web容器,如Tomcat、Jetty、Undertow等,无需外部Web服务器。这意味着您可以将应用程序打包成可执行的JAR文件,直接运行而不需要额外的容器配置,从而简化了部署过程。

4.微服务支持:Spring Boot与Spring Cloud结合使用,可以轻松快速构建和部署微服务架构。

5.依赖版本管理:Spring Boot帮助开发人员管理了常用第三方依赖的版本,防止出现版本冲突问题。这样,您可以更专注于业务逻辑,而不用担心依赖的版本兼容性。

6.监控和管理: Spring Boot自带了监控功能,包括应用程序运行状况监控、内存使用情况、线程池状态、HTTP请求统计等。此外,Spring Boot还提供了优雅关闭应用程序的方式,使得应用程序的管理更加便捷。

34.Spring和SpringBoot的关系和区别?

他们的关系是:

Spring是框架Spring Boot是个脚手架: Spring是一个全功能的Java应用程序框架,旨在帮助开发人员构建各种类型的应用程序,包括Web应用、企业级应用、批处理应用等。Spring提供了大量的组件和功能,但需要开发人员进行详细的配置和集成。Spring Boot则是一个脚手架工具,它基于Spring框架,旨在简化Spring应用程序的初始配置和开发过程,提供了自动化配置和约定优于配置的特性

Spring Boot构建在Spring之上: Spring Boot不是一个独立的框架,而是建立在Spring框架之上的工具。它使用了Spring的核心功能,如依赖注入、面向切面编程、事务管理等。因此,Spring Boot项目可以充分利用Spring框架的功能,但更容易启动和运行。

他们的区别是:

  1. 配置方式: Spring通常需要大量的XML配置或Java注解来定义应用程序的配置。相比之下,Spring Boot采用了"约定优于配置"的原则,大部分配置都可以通过默认值和自动配置来完成,从而减少了配置的复杂性。
  2. 开发速度: Spring Boot旨在提高开发速度,因此它降低了开发人员的学习曲线,并提供了内置的Starter和自动配置,使开发更加快速和高效。Spring需要更多的手动配置和集成工作。
  3. 内置Web容器: Spring Boot内置了多个内嵌式Web容器,如Tomcat、Jetty、Undertow等,可以轻松创建独立的可执行JAR文件或WAR文件,而不需要外部Web服务器。Spring通常需要外部Web服务器的部署。

35.SpringBoot的启动原理?

  1. 运行Main方法: 应用程序启动始于Main方法的执行。在Main方法中,创建了一个SpringApplication实例,用于引导应用程序的启动。同时,SpringApplication会根据spring.factories文件加载并注册监听器、ApplicationContextInitializer等扩展接口实现。
  2. 运行run方法: 运行SpringApplication的run方法是应用程序启动的入口。在这一步,Spring Boot会 启动Spring进而创建内置tomcat,进去run方法后还做了很多其他事:Spring Boot会读取和解析环境变量、配置文件(如application.properties或application.yml)等,以获取应用程序的配置信息。之后再创建ApplicationContext也就是我们熟知的Spring上下文: 在这一步,Spring Boot会根据应用程序的类型(例如,Web应用程序)创建相应的ApplicationContext。对于Web应用程序,通常创建的是ServletWebServerApplicationContext。
  3. 预初始化上下文: Spring Boot会将启动类作为配置类,读取并注册为BeanDefinition,这使得Spring容器可以识别应用程序的配置。
  4. 调用refresh: 此时,Spring Boot调用了refresh方法来加载和初始化Spring容器。在这一过程中,会执行一系列操作,包括解析@Import注解以加载自动配置类,创建和注册BeanDefinition等。
  5. 创建内置servlet容器: 如果应用程序是一个Web应用程序,Spring Boot会在这一步创建内置的servlet容器(例如Tomcat),以便应用程序可以接受HTTP请求。这个容器将被Spring Boot自动配置,并且可以通过配置进行自定义。
  6. 监听器和扩展点: 在整个启动过程中,Spring Boot会调用各种监听器和扩展点,这些组件可以用来对应用程序进行扩展和定制。例如,您可以使用监听器来处理应用程序启动和关闭事件,或者使用ApplicationContextInitializer来自定义ApplicationContext的初始化。

36.SpringBoot的自动装配?

引入starter

@SpringBootApplication

​ 1.@SpringBootConfiguration--->@Configuration

​ 2.@EnableAutoConfiguration--->@Import(importselector的实现类)importselector的实现类实现了selectimport方法,返回值是string数组,里面包含了要自动配置的所有类的全类名,该方法写导入了META-INFO/spring.factoriesMETA-INFO/spring/AutoConfiguration.imports两个文件,文件里就是要自动配置 的类的全类名(META-INFO/spring/AutoConfiguration.imports是springboot2.7.x之后新增的,在springboot3之后就彻底移除spring.factories)

​ 3.@ComponentScan:组件扫描,默认扫描当前包及其子包

用@Condition来过滤掉无效的自动配置类

37.SpringBoot特性的开启方式有以下几种:

1.使用@EnableAutoConfiguration注解开启自动配置特性。
2.使用@SpringBootApplication注解开启SpringBoot应用程序。
3.使用@Configuration注解和@Import注解手动导入需要的配置类。
4.继承spring-boot-starter-parent项目。

38.SpringMVC处理请求流程?有哪些组件?

image-20240822172720957

  1. 客户端发送请求
  2. DispatcherServlet拦截请求
  3. HandlerMapping匹配请求
  4. 调用处理器配置器HandlerAdapter
  5. 执行控制器中的业务逻辑,返回数据或ModelAndView
  6. 视图解析View Resolver
  7. 视图渲染
  8. 响应返回给客户端

39.ApplicationContext容器实现类四种

image-20240827235517931


MySQL数据库

1.数据库三范式了解吗?

数据库范式有 3 种:

2.主键和外键有什么区别?

3.CHAR 和 VARCHAR 的区别是什么?

CHAR 和 VARCHAR 是最常用到的字符串类型,两者的主要区别在于:CHAR 是定长字符串,VARCHAR 是变长字符串。

CHAR 在存储时会在右边填充空格以达到指定的长度,检索时会去掉空格;VARCHAR 在存储时需要使用 1 或 2 个额外字节记录字符串的长度,检索时不需要处理。

CHAR 更适合存储长度较短或者长度都差不多的字符串,例如 Bcrypt 算法、MD5 算法加密后的密码、身份证号码。VARCHAR 类型适合存储长度不确定或者差异较大的字符串,例如用户昵称、文章标题等。

CHAR(M) 和 VARCHAR(M) 的 M 都代表能够保存的字符数的最大值,无论是字母、数字还是中文,每个都只占用一个字符。

VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,不过,VARCHAR(100) 会消耗更多的内存。

4.整数类型的 UNSIGNED 属性有什么用?

UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。

对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。

5.DECIMAL 和 FLOAT/DOUBLE 的区别是什么?

DECIMAL 和 FLOAT 的区别是:DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。

DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。

在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 java.math.BigDecimal

6.DATETIME(datetime) 和 TIMESTAMP(timestamp) 的区别是什么?

DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。

TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

7.NULL‘’的区别是什么?

NULL''(空字符串)是两个完全不一样的值,区别如下:

看了上面的介绍之后,相信你对另外一个高频面试题:“为什么 MySQL 不建议使用 NULL 作为列默认值?”也有了答案。

8.Boolean类型如何表示?

MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。

9.常用的存储引擎有哪些?

  1. InnoDB
  2. MyISAM
  3. MEMORY

MySQL 5.5.5 之前,MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。

10.InnoDB和MyISAM区别?

1.存储方式

MyISAM使用非聚簇索引,索引文件和数据文件是分开的;而InnoDB使用聚簇索引,将索引和数据一起存储在同一个文件中。

2.锁机制

MyISAM 只有表级锁(table-level locking),而 InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。

3.是否支持事务

MyISAM 不提供事务支持。

InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别,具有提交(commit)和回滚(rollback)事务的能力。并且,InnoDB 默认使用的 REPEATABLE-READ(可重读)隔离级别是可以解决幻读问题发生的(基于 MVCC 和 Next-Key Lock)。

4.是否支持外键

MyISAM 不支持,而 InnoDB 支持。

5.是否支持数据库异常崩溃后的安全恢复

MyISAM 不支持,而 InnoDB 支持。

使用 InnoDB 的数据库在异常崩溃后,数据库重新启动的时候会保证数据库恢复到崩溃前的状态。这个恢复的过程依赖于 redo log

6.性能

InnoDB 的性能比 MyISAM 更强大。

总结:

innoDB:支持外键,事务,崩溃修复(redo log)。支持行锁,占用空间较大,默认使用B+数索引,读效率低,支持MVCC

MyISAM:不支持外键和事务,只有表锁,占用空间小,写入效率低,读效率高。B数索引

11.索引分类有哪些?

主键索引:主键索引是一种特殊的索引类型,它是用于唯一标识每一行数据的索引,每个表只能有一个主键索引。

唯一索引:唯一索引是用来保证列的唯一性的索引,一个表可以有多个唯一索引。

普通索引:普通索引也叫非唯一索引,它是最常见的一种索引类型,可以加速查询和排序操作。

全文索引:全文索引是一种用于全文搜索的索引类型,能够对文本数据进行快速的模糊搜索和关键字搜索。

复合索引:复合索引也叫多列索引或联合索引,它是包含多个列的索引类型,能够加速多列查询和排序操作。

哈希索引:哈希索引是基于哈希表实现的索引类型,能够对等值查询进行高效的处理,但不支持范围查询和排序,MySQL 中 Memory 引擎中支持哈希索引。

12.什么是索引?索引的优缺点?

什么是索引:

索引是数据库中用于提高数据检索性能排好序的数据结构。它类似于书籍的目录,通过建立特定的数据结构将列或多个列的值与它们在数据表中对应的行关联起来,以加快查询速度。

优点:

  1. 提高查询效率:索引可以加速数据的检索速度,对于大量数据的表而言,使用索引可以大幅提高查询效率。
  2. 避免全表扫描:当没有索引时,数据库会进行全表扫描,而索引可以帮助避免全表扫描,加速查询。
  3. 增强数据的唯一性和完整性:可以通过在列上创建唯一索引和主键索引来确保表中的数据唯一性和完整性。

缺点:

  1. 占用额外存储空间:每个索引都会占用额外的存储空间,因此在设计索引时需要权衡存储空间和查询效率之间的平衡。
  2. 降低写操作效率:索引的维护需要额外的写操作,因此在大量写操作的情况下可能会降低写操作的效率。
  3. 可能出现索引失效:索引并不是万能的,有些情况下使用索引可能会导致查询效率降低甚至出现索引失效的情况。例如,当对于一个非常小的表或者一个稠密的索引列进行查询时,使用索引可能并不会提高查询效率。
  4. 索引需要维护:随着表数据的不断变化,索引也需要不断维护以保持其效率。因此,在使用索引时需要注意索引的维护成本。

13.InnoDB在创建表时怎么自动创建索引的?

在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:

其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引

14.B树和B+树的区别?

  1. 数据存储方式:在B树中,每个节点都包含键和对应的值,叶子节点存储了实际的数据记录;而B+树中,只有叶子节点存储了实际的数据记录,非叶子节点只包含键信息和子节点的指针。

  2. 数据检索方式:在B树中,由于非叶子节点也存储了数据,所以查询时可以直接在非叶子节点找到对应的数据,具有更短的查询路径;而B+树的所有数据都存储在叶子节点上,只有通过叶子节点才能获取到完整的数据。

  3. 范围查询效率:由于B+树的所有数据都存储在叶子节点上,并且叶子节点之间使用链表连接,所以范围查询的效率较高。而在B树中,范围查询需要通过遍历多个层级的节点,效率相对较低。

  4. 适用场景:B树适合进行随机读写操作,因为每个节点都包含了数据;而B+树适合进行范围查询和顺序访问,因为数据都存储在叶子节点上,并且叶子节点之间使用链表连接,有利于顺序遍历。

15.MySQL多表查询时有哪些连接方式?

  1. 内连接(INNER JOIN):返回同时满足连接条件的行。它通过比较连接列的值,将两个或多个表中匹配的行组合在一起。
  2. 左外连接(LEFT JOIN):返回左表中的所有行,以及与左表匹配的右表的行。如果右表中没有匹配的行,对应的列将填充为 NULL。
  3. 右外连接(RIGHT JOIN):返回右表中的所有行,以及与右表匹配的左表的行。如果左表中没有匹配的行,对应的列将填充为 NULL。
  4. 全外连接(FULL JOIN):返回左右两个表中的所有行。如果某个表中没有匹配的行,对应的列将填充为 NULL。需要注意 MySQL 不支持 FULL JOIN 可以使用UNION ALL 模拟。
  5. 自连接(Self JOIN):将单个表视为两个独立的表,使用别名来引用同一个表。这种连接适用于在同一个表中根据某些条件关联不同的行。
  6. 交叉连接(CROSS JOIN):返回两个表的笛卡尔积,即所有可能的组合。它将第一个表的每一行与第二个表的每一行进行组合。

16.什么是回表操作?

如果我用 product_no 二级索引查询商品,如下查询语句:

select * from product where product_no = '0002';

会先检二级索引中的 B+Tree 的索引值(商品编码,product_no),找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据

17.什么是最左前缀原则?

它指的是在使用复合索引时,索引的最左边的连续几个列会被用于查询过滤条件的匹配。

比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。

但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

上面这些查询条件之所以会失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。

遵循最左前缀原则的好处包括:

  1. 提高查询性能:通过使用索引的最左前缀,可以最大限度地减少索引扫描的数据量,提高查询的效率和响应时间。
  2. 减少索引占用空间:在某些情况下,使用最左前缀原则可以减少创建多个索引的需求,节省磁盘空间和索引维护的开销。

18.什么是覆盖索引?

覆盖索引是指一个索引包含了查询所需的所有列,而无需访问表的实际数据页。无需回表操作,减少磁盘IO,提高响应速度。

覆盖索引通常适用于以下场景:

  1. 查询语句只需要返回索引列中的数据,而不需要访问其他列的值。
  2. 查询语句中的条件过滤、排序或分组的列都在同一个索引上。

19.什么是索引下推?

musql5.6针对扫描二级索引的优化改进,把索引的过滤条件下推到存储引擎,适用于myisam和innoDB。减少回表操作。

具体:

可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

假设有一个包含许多列的表 products,并为复合索引 (category_id, price) 创建以下查询:

SELECT * FROM products WHERE category_id = 5 AND price < 100;

如果复合索引 (category_id, price) 存在:
无索引下推
MySQL根据category_id = 5条件使用索引检索数据,然后访问主表的每一行以评估price < 100条件。这可能导致访问许多不满足价格条件的行。
有索引下推
MySQL在索引层面直接评估price < 100的条件,只需将符合条件的行从表中提取。这减少了对表的I/O操作。

适用场景:

索引下推非常适合于查询条件中涉及多个列的过滤,且这些列存在复合索引的情况下。它可以在索引扫描过程中应用额外的过滤条件来减少所需的行访问。

20.为什么需要数据库连接池呢?

  1. 提高性能:数据库连接的建立和断开是比较耗时的操作,频繁地创建和销毁连接会增加系统的负担。通过使用连接池,可以避免频繁地创建和关闭连接,减少了连接的开销,提高了系统的性能。
  2. 资源管理:数据库连接是有限的资源,如果每个请求都创建一个新的连接,可能导致连接过多而耗尽资源。连接池通过对连接的管理和复用,能够更有效地管理数据库连接,避免资源的浪费。
  3. 并发处理:在高并发的场景下,如果每个请求都去单独连接数据库,可能会导致数据库连接数量过多,从而限制了系统的扩展性。连接池允许多个请求共享连接,减少了数据库连接的数量,提高了并发处理能力。
  4. 连接可靠性:数据库连接可能会因为网络问题或服务器故障而中断,当发生这种情况时,连接池能够检测到连接的失效,并重新创建一个可用的连接,确保应用程序的可靠运行。

21.并发事务带来哪些问题?

  1. 脏读(Dirty Read)一个事务读取了另一个事务未提交的数据。假设事务A修改了一条数据但未提交,事务B却读取了这个未提交的数据,导致事务B基于不准确的数据做出了错误的决策。
  2. 不可重复读(Non-repeatable Read)一个事务在多次读取同一数据时,得到了不同的结果。假设事务A读取了一条数据,事务B修改或删除了该数据并提交,然后事务A再次读取同一数据,发现与之前的读取结果不一致,造成数据的不一致性。
  3. 幻读(Phantom Read)一个事务在多次查询同一范围的数据时,第二次比第一次多(少)查询到了新的数据。假设事务A根据某个条件查询了一组数据,事务B插入了符合该条件的新数据并提交,然后事务A再次查询同一条件下的数据,发现结果集发生了变化,产生了幻觉般的新增数据。
  4. 丢失修改(Lost Update):两个或多个事务同时修改同一数据,并且最终只有一个事务的修改被保留,其他事务的修改被覆盖或丢失。这种情况可能会导致数据的部分更新丢失,造成数据的不一致性。

幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

22.UNION 与UNION ALL 的区别?

  1. UNION:UNION用于合并两个或多个查询结果集,并去除重复的行。它将多个查询的结果合并为一个结果集,并自动去除重复的行。在执行UNION操作时,数据库会进行额外的去重操作,这可能会带来一定的性能开销。
  2. UNION ALL:UNION ALL同样用于合并查询结果集,但不去除重复的行。它将多个查询的结果简单地合并在一起,包括重复的行。相比于UNION,UNION ALL不进行去重操作,因此执行效率更高

23.MySQL中like模糊查询如何优化?

1. 合理使用索引:

前缀匹配:使用LIKE 'prefix%'的形式,这种情况MySQL能够利用索引,比如:

SELECT * FROM users WHERE username LIKE 'John%';

如果username字段有索引,前缀匹配会用到索引。

2.使用反向索引:

对于需要匹配后缀的情况(即LIKE '%suffix'),可以创建一个辅助列存储反转字符串,并基于此列进行前缀匹配。

​ 创建反向字符串:

ALTER TABLE users ADD reversed_username VARCHAR(255);  
UPDATE users SET reversed_username = REVERSE(username);  
CREATE INDEX idx_reversed_username ON users(reversed_username);

3. 限制扫描范围:

在LIKE查询中,如果可以通过其他条件进一步缩小搜索范围,请尽量加入这些条件。

SELECT * FROM users WHERE created_at >= '2023-01-01' AND username LIKE 'John%';

4. 使用缓存

5. 使用专业工具:

对于非常大的数据集或者需要复杂文本处理和搜索功能,可以使用外部全文搜索引擎如Elasticsearch、Solr或者Sphinx来代替MySQL的LIKE操作。

24.count(1)、count(*)与count(列名) 的区别?

count(1)和count(*)几乎一样,是统计表中所有行的数量,值是否为 NULL。

count(列名) 统计指定列中非 NULL 值的行数。

性能对比

25.mysql中九种索引失效场景

  1. 不符合最左匹配原则
  2. 使用like并且左边带%(不正确的like查询)
  3. where条件里对索引列进行运算或使用函数
  4. 使用or且存在非索引列
  5. 使用order by可能导致索引失效
  6. 使用is null,is not null和不等于可能导致索引失效
  7. 数据量太少的情况也可能导致索引失效
  8. 索引列进行了类型转换
  9. select * 导致索引失效

26.查慢sql的原因

  1. 启用慢查询日志,找到时间长的sql
  2. 检查索引使用情况 show index
  3. 分析查询语句:看有没有冗余操作,重复子查询,大量join操作等
  4. 使用EXPLAIN分析执行计划:对于慢查询的SQL语句,使用EXPLAIN命令来查看其执行计划。通过分析执行计划,确定查询是否有效利用了索引以及是否存在性能瓶颈。

27.优化慢SQL

  1. 优化查询语句:比如不使用select *,只查询需要的字段。
  2. 优化表结构和数据类型:单表不要有太多字段,建议在 20 个字段以内,使用可以存下数据最小的数据类型,尽可能使用 not null 定义字段,因为 null 占用 4 字节空间。
  3. 建立并使用索引
  4. 使用覆盖索引
  5. 优化复合索引,并确保索引顺序符合查询使用模式的最左前缀原则
  6. 避免过多索引,过多的索引会影响写性能,选择对查询影响最大的列进行索引。
  7. 使用 JOIN 替代子查询
  8. 定期维护和重建索引以防止索引碎片影响性能
  9. 避免全表扫描
  10. 读写分离

28.意向锁

A加行锁,B之后要加表锁

1.无意向锁的话,B会成功,A的行锁失效

2.有意向锁的话,B会失败

29.mysql中有哪几种锁?

  1. 表级锁(Table-level Locking):在事务操作中对整个表进行加锁。当一个事务对表进行写入操作时,其他事务无法对该表进行任何读写操作。表级锁通常是针对特定的DDL操作或备份操作。
  2. 共享锁(Shared Lock):也称为读锁(Read Lock),用于允许多个事务同时读取同一资源,但禁止并发写入操作。其他事务可以获取共享锁,但无法获取排他锁。
  3. 排他锁(Exclusive Lock):也称为写锁(Write Lock),用于独占地锁定资源,阻止其他事务的读写操作。其他事务无法获取共享锁或排他锁,直到持有排他锁的事务释放锁。
  4. 行级锁(Row-level Locking):也称为记录锁(Record Locking),在事务操作中对数据行进行加锁。行级锁可以控制并发读写操作,不同事务之间可以并发地访问不同行的数据。MySQL的InnoDB存储引擎默认使用行级锁。
  5. 记录锁(Record Lock):用于行级锁的一种形式,锁定数据库中的一个记录(行)以保证事务的隔离性和完整性。
  6. 间隙锁(Gap Lock):用于行级锁的一种形式,锁定两个记录之间的间隙。它可以防止其他事务在该间隙中插入新记录,从而保证数据的一致性。
  7. 临键锁(Next-Key Locks): 临键锁是记录锁和间隙锁的结合,锁定的是一个范围,并且包括记录本身。

30.说下你对数据库事务的理解?

数据库事务是指一系列数据库操作的逻辑单元,这些操作要么全部成功执行,要么全部回滚。它的目的是确保数据的一致性和完整性。

事务具备4大特性,即原子性一致性隔离性持久性:(ACID

31.事务的隔离级别有哪些?

  1. 读未提交:最低的隔离级别。事务可以读取到其他事务尚未提交的数据,可能会出现脏读、不可重复读和幻读问题。
  2. 读已提交:事务只能读取到已经提交的数据。但在同一事务中,多次读取同一行的数据结果可能会不一致,可能会出现不可重复读和幻读问题。
  3. 可重复读:在同一个事务内,多次读取同一行的数据结果始终保持一致。MySQL的默认隔离级别就是可重复读。通过使用MVCC机制来实现,在读操作期间不会看到其他事务的修改,有效地解决了不可重复读问题。
  4. 串行化:最高的隔离级别。事务串行执行,避免了脏读、不可重复读和幻读等问题。但是并发性能较差,可能导致大量的锁竞争和资源争用。

32.mysql的三大日志

二进制日志 binlog(归档日志)和事务日志 redo log(重做日志)和 undo log(回滚日志)。

redo log

redo log是innoDB独有的,让mysql拥有了崩溃恢复能力。实现了事务中的**持久性,主要用于掉电等故障恢复**;

undo log

undo log是 Innodb 存储引擎层生成的日志,实现了事务中的**原子性,主要用于事务回滚和 MVCC**。

是逻辑日志,记录相反的操作记录

一条记录的每一次更新操作产生的 undo log 格式都有一个 roll_pointer 指针和一个 trx_id 事务id:

bin log

二进制日志,记录了数据库中所有数据的更改操作。

是 Server 层生成的日志,主要==用于数据备份和主从复制==;

33.redo log 和 undo log 区别在哪?

34.redo log 要写到磁盘,数据也要写磁盘,为何要多此一举?

写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写

磁盘的「顺序写 」比「随机写」 高效的多,因此 redo log 写入磁盘的开销更小。

产生的 redo log 是直接写入磁盘的吗?:

实际上, 执行一个事务的过程中,产生的 redo log 也不是直接写入磁盘的,因为这样会产生大量的 I/O 操作,而且磁盘的运行速度远慢于内存。

所以,redo log 也有自己的缓存—— redo log buffer,每当产生一条 redo log 时,会先写入到 redo log buffer,后续在持久化到磁盘

35.如果不小心整个数据库的数据被删除了,能使用 redo log 文件恢复数据吗?

不可以使用 redo log 文件恢复,只能使用 binlog 文件恢复。

因为 redo log 文件是循环写,是会边写边擦除日志的,只记录未被刷入磁盘的数据的物理日志,已经刷入磁盘的数据都会从 redo log 文件里擦除。

binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。

36.binlog和redo log有什么区别?

  1. 适用对象不同:
    • binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
    • redo log 是 Innodb 存储引擎实现的日志;
  2. 写入方式不同:
    • binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
    • redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
  3. 用途不同:
    • binlog 用于备份恢复、主从复制;
    • redo log 用于掉电等故障恢复。

37.主从复制是怎么实现?

38.事务四大特性ACID都是通过什么实现的?

  1. 隔离性:MVCC+锁
  2. 一致性:redo log+undo log
  3. 持久性:redo log
  4. 原子性:undo log

39.MVCC

MVCC是多版本并发控制,维护一个数据的多个版本,使读写无冲突。

是事务隔离级别无锁的实现方式,用于提高事务的并发性能。

MVCC实现原理:

  1. 隐藏字段(三个)
    • 最近修改事务的ID
    • 回滚指针,指向记录的上一个版本
    • 隐藏主键,当没有主键时才会生成该字段
  2. Undo log版本链
  3. ReadView

当前读:读的是记录的最新版本,会对记录加锁

快照读:简单的select就是快照读,读的是数据的可见版本,有可能是历史数据,不加锁

读已提交:每次select都生成快照读,每次快照读都生成一个新的ReadView

可重复度:事务中第一个select才是快照读的地方,事务中第一次快照读时才生成ReadView,后续复用该ReadView

串行化:快照读会退化成当前读

在 Read View 中包含了以下 4 个主要的字段:

  1. m_ids:当前活跃的事务编号集合。
  2. min_trx_id:最小活跃事务编号。
  3. max_trx_id:预分配事务编号,当前最大事务编号+1。
  4. creator_trx_id:ReadView 创建者的事务编号。

40.为什么要有 Buffer Pool?

虽然说 MySQL 的数据是存储在磁盘里的,但是也不能每次都从磁盘里面读取数据,这样性能是极差的。

要想提升查询性能,加个缓存就行了嘛。所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。

为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

41.mysql主从复制流程

  1. 主数据库接收到一个写操作(如 INSERT、UPDATE、DELETE)时,会将这个操作记录到二进制日志(Binary Log)中,将数据修改的操作按顺序记录下来。
  2. 从数据库 IO 线程会自动连接主服务,从二进制中读取同步数据,记录到中继日志(Relay Log)中。
  3. 从数据库的 SQL 线程会定期从中继日志中获取同步数据,写入到从数据库中。

image-20240823114038668

42.说一下你对行锁,临键锁,间隙锁的理解?

  1. 行锁

行锁是针对单条记录的锁,也称为记录锁。行锁用于精确锁定表中的某一行数据,确保在同一时刻,只有一个事务能够对该行数据进行修改。

适用场景:行锁通常用于SELECT FOR UPDATEUPDATEDELETE语句,这些语句会锁定相关的行,防止其他事务同时修改。

优点:行锁粒度小,锁定范围有限,因此并发性能较好。

  1. 间隙锁

间隙锁是指锁住两个索引记录之间的间隙,防止其他事务在该间隙中插入新记录。

当事务执行范围查询时,InnoDB 会在需要时对两个记录之间的空隙加锁,防止其他事务插入记录。间隙锁通常与临键锁一起使用。

应用:间隙锁主要用于范围查询(如 SELECT ... FOR UPDATE)中,确保查询的范围在事务期间不发生变化。

优点:避免幻读

  1. 临键锁

它结合了行锁和间隙锁。临键锁不仅锁住查询命中的记录,还会锁住这些记录前面的间隙,以防止其他事务插入新的记录。

应用:范围查询

优点:避免幻读


Redis

1.redis为什么这么快?

  1. 数据存储在内存中:Redis 的数据存储在内存中,而内存的读写速度远远快于硬盘。这使得 Redis 能够实现非常快速的读写操作。
  2. 单线程处理请求:Redis 是单线程的,因此可以避免线程切换和锁竞争等问题,提高了 CPU 的利用率和性能。
  3. 高效的数据结构:Redis 提供了多种高效的数据结构,如哈希表、有序集合等,这些数据结构能够快速地进行插入、删除、查找和排序等操作。
  4. 异步 I/O:Redis 使用异步 I/O 技术,可以在等待客户端输入或输出时继续处理其他请求,从而提高了系统的吞吐量。
  5. 高效的持久化机制:Redis 提供了多种持久化机制,如 RDB、AOF 和混合持久化机制,这些机制运行都非常高效,可以在不影响性能的情况下保证数据的安全。

2.redis可以实现哪些功能?

  1. 缓存:Redis 可以作为缓存系统,将热点数据存储在内存中,提高读写性能和响应速度,减少对后端数据存储的压力。
  2. 消息队列:Redis 的发布订阅功能和 List 数据结构可以实现消息队列的功能,实现异步处理任务、解耦系统组件之间的依赖关系等。
  3. 计数器和排行榜:Redis 的原子操作和 Sorted Set 数据结构可以实现计数器和排行榜的功能,支持快速地增加、减少和排序操作。
  4. 分布式锁:Redis 的 SETNX 命令可以实现分布式锁,避免多个客户端同时修改同一个数据,保证数据的一致性和正确性。
  5. 分布式会话管理:Redis 可以存储会话信息,实现分布式会话管理,支持会话的共享和迁移等功能。

主要还是缓存和分布式锁

3.redis有哪些数据类型?

五种基础数据类型:

  1. ==String(字符串类型)==常见使用场景是:存储 Session 信息、存储缓存信息(如详情页的缓存)、存储整数信息,可使用 incr 实现整数+1,和使用 decr 实现整数 -1;
  2. ==List(列表类型)==常见使用场景是:实现简单的消息队列、存储某项列表数据;
  3. ==Hash(哈希表类型)==常见使用场景是:存储 Session 信息、存储商品的购物车,购物车非常适合用哈希字典表示,使用人员唯一编号作为字典的 key,value 值可以存储商品的 id 和数量等信息、存储详情页信息;
  4. ==Set(集合类型)==是一个无序并唯一的键值集合,它的常见使用场景是:关注功能,比如关注我的人和我关注的人,使用集合存储,可以保证人员不会重复;
  5. ==Sorted Set(有序集合类型)==相比于 Set 集合类型多了一个排序属性 score(分值),它的常见使用场景是:可以用来存储排名信息、关注列表功能,这样就可以根据关注实现排序展示了。

还有三种特殊数据类型:

HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。

image-20240901211424464

4.三种特殊数据类型

地理位置Geospatial

geo 的数据类型为 zset

GEO 的数据结构总共有六个常用命令:geoadd、geopos、geodist、georadius、georadiusbymember、gethash

  1. GEOADD:将指定的经度和纬度信息添加到指定的键中。其语法如下:

  2. GEOPOS:获取指定地点的经度和纬度信息。其语法如下:

  3. GEODIST:计算两个地点之间的距离。其语法如下:

  4. GEORADIUS:根据指定的中心地点和半径范围,获取在这个范围内的地点信息。其语法如下:

  5. GEORADIUSBYMEMBER:根据指定的地点和半径范围,获取在这个范围内的地点信息。其语法类似于 GEORADIUS 命令,但是它使用地点名称或标识作为参数,而不是经纬度。

  6. GEOHASH:获取指定地点的 Geohash 值。Geohash 是地理位置编码系统,用于将地理位置信息编码为一串字符串。在 Redis 中,GEOHASH 命令不是原生的命令,但可以通过使用 GEOPOS 命令获取经纬度信息,然后使用其他编程语言中的 Geohash 库进行编码。

基数统计 HyperLogLog

什么是基数

比如数据集 A {1,3,5,7,5,7,8},那么这个数据集的基数集为 {1, 3, 5 ,7, 8},基数(不重复元素)为 5。基数估计就是在误差可接受的范围内,快速计算基数。

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。HyperLogLog 则是一种算法,它提供了不精确的去重计数方案。

[PFADD key element [element ...]添加元素

[PFCOUNT key [key ...]查看基数数量

[PFMERGE destkey sourcekey [sourcekey ...]合并基数

位图Bitmap

位存储

在开发中,可能会遇到这种情况:需要统计用户的某些信息,如活跃或不活跃,登录或者不登录;又如需要记录用户一年的打卡情况,打卡了是 1, 没有打卡是 0,如果使用普通的 key-value 存储,则要记录 365 条记录,如果用户量很大,需要的空间也会很大,所以 Redis 提供了 Bitmap 位图这种数据结构。

Bitmap 就是通过操作二进制位来进行记录,即为 0 和 1;如果要记录 365 天的打卡情况,使用 Bitmap表示的形式大概如下:0101000111000111...........................,这样有什么好处呢?当然就是节约内存了,365 天相当于 365 bit,又 1 字节 = 8 bit , 所以相当于使用 46 个字节即可。

BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态,其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现。Redis 从 2.2 版本之后新增了 setbit,getbit, bitcount 等几个 bitmap 相关命令。

添加元素:

SETBIT key offset value : 设置 key 的第 offset 位为value (1或0)

获取元素:

GETBIT key offset 获取offset设置的值,未设置过默认返回0

统计元素:

bitcount key [start, end] 统计 key 上位为1的个数

5.String 还是 Hash 存储对象数据更好呢?

在绝大部分情况,我们建议使用 String 来存储对象数据即可!

6.有序集合 (Sorted Set)底层是如何实现的?

有序集合是由 ziplist (压缩列表)skiplist (跳跃表) 组成的。

当数据比较少时,有序集合是压缩列表 ziplist 存储的,反之则为跳跃表 skiplist 存储,使用压缩列表存储必满足以下两个条件:

  1. 有序集合保存的元素个数要小于 128 个;
  2. 有序集合保存的所有元素成员的长度都必须小于 64 字节。

如果不能满足以上两个条件中的任意一个,有序集合将会使用跳跃表 skiplist 结构进行存储。

image-20240831222512930

7.什么是缓存穿透?如何解决?

缓存穿透是指在缓存系统中,大量的请求查询不存在于缓存和数据库中的数据,导致这些请求直接访问数据库,占用数据库资源,而缓存无法发挥作用的现象。

解决

  1. 缓存空对象:对于查询数据库返回的空结果,也可以将空结果缓存起来,设置一个较短的过期时间,避免频繁查询数据库。这样在下次查询相同的数据时,可以直接从缓存中获取空结果,而不需要再次查询数据库。
  2. 布隆过滤器:在缓存层引入布隆过滤器,可以在查询请求到达时,首先通过布隆过滤器判断该请求对应的数据是否存在于缓存或数据库中,从而避免无效的查询操作。
  3. 限制恶意请求:通过访问频率控制、验证码等手段,限制对缓存的恶意请求,防止攻击者通过查询不存在的数据来触发缓存穿透。

8.什么是缓存击穿?如何解决?

缓存击穿是指在缓存系统中,某个热点数据过期或失效时,同时有大量的请求访问该数据,导致请求直接访问数据库或后端服务,给数据库或后端服务造成巨大压力,导致系统性能下降甚至崩溃的现象。

解决:

  1. 设置热点数据永不过期
  2. 加互斥锁:在缓存失效后,通过加锁控制对数据库的访问,从而保证只有一个请求能够去查询数据库并更新缓存。但存在问题,会导致1个线程在重建缓存而其他所有线程阻塞。
  3. 逻辑过期+互斥锁:设置逻辑过期时间,每次自己判断是否到过期时间,如果到了则返回过期数据并新建1个线程去重建数据,别的线程没拿到锁也直接返回旧数据而不是阻塞。

9.什么是缓存雪崩?如何解决?

缓存雪崩是指在某一时刻,大量缓存数据共同过期或者redis宕机,导致瞬时大流量涌向数据库。

解决

  1. 缓存数据过期时间设置为随机:把key的过期时间后加一小段随机事件,防止同时过期
  2. 缓存预热
  3. 用redis集群提高服务可用性
  4. 热点数据永不过期

10.什么是布隆过滤器?如何实现?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否在一个集合中。它基于位数组和多个哈希函数的原理,可以高效地进行元素的查询,而且占用的空间相对较小,如下图所示:

img

布隆过滤器说一个元素不在集合中,那么它一定不在这个集合中;但如果它说一个元素在集合中,则有可能是不存在的(存在误差)

特点

  1. 数据检索性能快
  2. 占用内存小
  3. 适用于大数据量,存在一定的误判

布隆过滤器执行过程

  1. 在 Redis 中创建一个位数组,用于存储布隆过滤器的位向量。
  2. 初始化多个哈希函数,并将每个哈希函数的计算结果对应的位数组位置设置为 1。
  3. 添加元素到布隆过滤器时,对元素进行多次哈希计算,并将对应的位数组位置设置为 1。
  4. 查询元素是否存在时,对元素进行多次哈希计算,并检查对应的位数组位置是否都为 1。

适用场景

  1. 大数据量去重:可以用布隆过滤器来进行数据去重,判断一个数据是否已经存在,避免重复插入。
  2. 缓存穿透:可以用布隆过滤器来过滤掉恶意请求或请求不存在的数据,避免对后端存储的频繁访问。
  3. 网络爬虫的 URL 去重:可以用布隆过滤器来判断 URL 是否已经被爬取,避免重复爬取。

底层原理:

bitmap位图

存储一个字符串比如lmj,就需要hsah("lmj")%n(n为bitmap长度)取模的值就是存储的bitmap的位置,但是可能会有hash碰撞,所以布隆过滤器对一个字符串使用多个hash算法给数据存到每个取模得到的位置上,判断的时候要所有位置都是1才能证明数据存在,只要有一个位置是0就说明数据不存在,这样能有效地减少误判的几率,但误判还是会存在的。(不存在一定不存在,存在不一定存在)

如何减小误判的几率:

1.增加bitmap的长度

2.增加hash个数(但会让我们的布隆过滤器性能有所降低)

redission提供了布隆过滤器

RBloomFilter bloomFilter=redission.getBloomFilter("key")

bloomFilter.tryInit(估算的bitmap位数,容错率)

之后就可以bloomFilter.add(元素)了

用bloomFilter.contains(元素)来判断元素存不存在

11.redis过期删除策略有哪些?

  1. 惰性删除:(默认)当客户端尝试访问一个已过期的键时,Redis会立即将该键删除,并返回空值。这种策略的优点是删除操作是在需要时进行,减少了不必要的删除开销。但是,如果大量过期键在一次性被访问之前没有被访问过,这些键会一直占据内存空间。
  2. 定期删除:Redis会每隔一段时间执行一次检查,删除那些已过期的键。默认情况下,Redis每秒执行10次检查。定期删除通过释放过期键所占据的内存空间,使得内存能够及时被回收。但这种方式可能会导致内存占用较高,因为Redis并不保证在每次定期删除操作中都会删除足够数量的过期键。

Redis 定期删除流程如下:

  1. 从设置了过期时间的字典中随机取出 20 个键;
  2. 删除这 20 个键中过期的键;
  3. 如果过期 key 的比例超过 25% ,重复步骤 1。

同时为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。

12.redis的内存淘汰策略了解吗?

  1. volatile-random:随机淘汰设置了过期时间的任意键值。这种策略适用于数据集大小较大,需要保持数据随机性的情况。

  2. volatile-ttl:优先淘汰更早过期的键值。这种策略适用于数据集大小较小,需要保持数据及时更新的情况。

  3. volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值。这是Redis 3.0之前默认的内存淘汰策略,适用于数据集大小较小,需要保持数据的访问频率的情况。

  4. volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值。这是Redis 4.0后新增的内存淘汰策略,适用于数据集大小较小,需要保持数据的访问频率的情况。
    在所有数据范围内进行淘汰:

  5. allkeys-random:随机淘汰任意键值。这种策略适用于数据集大小较大,需要保持数据随机性的情况。

  6. allkeys-lru:淘汰整个键值中最久未使用的键值。这种策略适用于数据集大小非常大的情况。

  7. allkeys-lfu:淘汰整个键值中最少使用的键值。这是Redis 4.0后新增的内存淘汰策略,适用于数据集大小非常大的情况。

13.redis持久化机制

RDB和AOF

RDB(Redis DataBase)

rdb保存的文件是dump.rdb

RDB是一种快照持久化的方式,它会将Redis在某个时间点的数据状态以二进制的方式保存到硬盘上的一个文件中。RDB持久化可以通过配置定时或手动触发,也可以设置自动触发的条件。RDB的优点是生成的文件比AOF文件更小,恢复速度也更快,适合用于备份和灾难恢复。

优点:

1.适合大规模的数据恢复
2.对数据的完整性要求不高
缺点:

1.需要一定的时间间隔进行操作,如果redis意外宕机了,这个最后一次修改的数据就没有了。
2.fork进程的时候,会占用一定的内容空间。

AOF(Append Only File)

aof保存的文件是appendonly.aof

快照功能(RDB)并不是非常耐久(durable): 如果 Redis 因为某些原因而造成故障停机, 那么服务器将丢失最近写入、以及未保存到快照中的那些数据。 从 1.1 版本开始, Redis 增加了一种完全耐久的持久化方式: AOF 持久化。

AOF以日志的形式来记录每个写的操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

触发条件:AOF 文件的更新可以由多种条件触发:

优点:

  1. AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis 最多也就丢失 1 秒的数据而已。

  2. AOF 文件使用 Redis 命令追加的形式来构造,因此,即使 Redis 只能向 AOF 文件写入命令的片断,使用 redis-check-aof 工具也很容易修正 AOF 文件。

  3. AOF 文件的格式可读性较强,这也为使用者提供了更灵活的处理方式。例如,如果我们不小心错用了 FLUSHALL 命令,在重写还没进行时,我们可以手工将最 后的 FLUSHALL 命令去掉,然后再使用 AOF 来恢复数据。

缺点:

  1. 对于具有相同数据的的 Redis,AOF 文件通常会比 RDB文件体积更大
  2. 虽然 AOF 提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。但在 Redis 的负载较高时,RDB 比 AOF 具好更好的性能保证。
  3. RDB 使用快照的形式来持久化整个 Redis 数据,而 AOF 只是将每次执行的命令追加到 AOF 文件中,因此从理论上说,RDB 比 AOF 方式更健壮。官方文档也指出,AOF 的确也存在一些 BUG,这些 BUG 在 RDB 没有存在。

AOF和RDB混合持久化:

虽然跟 AOF 相比,RDB 快照的恢复速度快,但快照的频率不好把握,如果频率太低,两次快照间一旦宕机,就可能有比较多的数据丢失。如果频率太高,又会产生额外开销,那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?

在 Redis4.0 提出了混合使用 AOF 和 RDB 快照的方法,也就是两次 RDB 快照期间的所有命令操作由 AOF 日志文件进行记录。这样的好处是 RDB 快照不需要很频繁的执行,可以避免频繁 fork 对主线程的影响,而且 AOF 日志也只记录两次快照期间的操作,不用记录所有操作,也不会出现文件过大的情况,避免了重写开销。

14.RDB和AOF有什么区别?

  1. 写入方式:RDB 是通过快照(snapshot)机制,将 Redis 中的数据集以二进制文件的方式写入硬盘;AOF 则是通过将 Redis 服务器执行的所有写命令(例如 set、del、incrby 等)记录在 AOF 文件中,写入方式是追加写入。
  2. 数据恢复:当 Redis 重启时,可以根据 RDB 文件或 AOF 文件来恢复数据。恢复 RDB 文件比恢复 AOF 文件快,因为 RDB 文件包含了一个时间点上的快照,可以直接将整个数据集加载到内存中。而恢复 AOF 文件则需要逐条执行文件中记录的命令,需要更长的时间。
  3. 数据完整性:RDB 文件保存的是 Redis 在某个时间点的数据快照,如果 Redis 在快照操作之后宕机,可能会丢失最后一次快照后的数据。而 AOF 文件记录了 Redis 所有的写命令,因此即使 Redis 宕机,也可以根据 AOF 文件恢复数据。
  4. 文件大小RDB 文件通常比 AOF 文件小,因为它只保存了一个时间点的数据快照,而 AOF 文件保存了所有的写命令,会比 RDB 文件大。
  5. 性能影响AOF 文件追加写入方式可能会降低 Redis 的写性能,但可以提供更好的数据安全性,而 RDB 文件在进行快照时可能会阻塞 Redis 的服务。

15.如何关闭redis持久化?

都是基于redis.conf文件

关闭RDB持久化

save ""  # 将 save 参数列表清空,表示不进行任何条件下的数据保存

关闭AOF持久化

appendonly no  # 设置为 no,表示关闭 AOF 持久化

关闭混合持久化

rdb-aof-use-rdb-preamble no # no 表示关闭混合持久化

16.redis如何实现分布式锁?

SETNX 和 EXPIRE 命令来实现

SETNX 是 "SET if Not eXists" 的缩写

SETNX 用于在指定的 key 不存在时设置 key 的值。如果 key 已经存在,SETNX 操作将不做任何事情,返回失败;如果 key 不存在,SETNX 操作会设置 key 的值,并返回成功。而 EXPIRE 是设置锁的过期时间的,主要为了防止死锁的发生。

存在的误删问题:

比如线程 1 设置的过期时间为 5s,而线程 1 执行了 7s,那么在第 5s 之后锁过期了,那么其他线程就可以拥有这把锁了,之后线程 1 执行完业务,又执行了锁删除操作,那么此时锁就被误删了。

解决方案:

此时可以每个锁的 value 中添加拥有者的标识,删除之前先判断是否是自己的锁,如果是则删除,否则不删除。但是判断和删除之间不是原子性操作,所以依然有问题。此时可以使用 lua 脚本来判断并删除锁,lua 脚本可以保证 redis 中多条语句执行的原子性,所以就可以解决此问题了。

所有存在的问题:

死锁问题:SETNX 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。

锁误删问题:SETNX 设置了超时时间,但因为执行时间太长,所以在超时时间之内锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。

不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。

无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。

Redission分布式锁

  1. Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。
  2. Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
  3. Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。

redission的分布式锁相比于setnx的锁的优点:

  1. 可重入:利用hash结构记录线程标识和重入次数
  2. 可重试:利用信号量控制可重试
  3. 解决超时释放问题:看门狗机制

image-20240824171035762

17.看门狗机制(watch dog)

如果一个线程获取锁后,运行程序到释放锁所花费的时间大于锁自动释放时间(也就是看门狗机制提供的超时时间30s),那么Redission会自动给redis中的目标锁延长超时时间。看门狗每隔 10秒 检查一次,如果发现客户端仍然持有锁,就会自动续约,将锁的过期时间重置为 30秒

18.联锁multilock

multiLock解决分布式锁主从一致性问题:不设置一台redis主机多台从机,而是设置多台redis主机,我们获取锁需要向每个主机都获取锁,multiLock就是把我们每个redis主机的锁一起拿来做成联锁。
内部原理就是把多个主机锁放入一个集合之中,遍历该集合,一个一个的获取每个锁,把获取成功的锁放入一个成功获取的锁的集合,当遍历过程中有一个锁没获取成功,我们就重新再来遍历锁集合,一个一个重新获取每个锁,直到要么所有锁都获取到了,要么就获取锁超时了退出。最后如果每个锁都获取成功的话,我们最后还要遍历成功获取的锁的集合,把每个锁的超时时间重置一下,因为第一个获取到的和最后一个获取到的锁的过期时间肯定不一致,最后这一步就是为了保证所以锁的过期时间一致(当然如果我们没有设置过期时间,默认为-1,就是开启看门狗机制就不需要最后这步重置过期时间操作,因为看门狗机制会无限自动续期锁的过期时间)。

19.redLock红锁

RedLock 算法旨在解决单个 Redis 实例作为分布式锁时可能出现的单点故障问题,通过在多个独立运行的 Redis 实例上同时获取锁的方式来提高锁服务的可用性和安全性。

RedLock 是对集群的每个节点进行加锁,如果大多数节点(N/2+1)加锁成功,则才会认为加锁成功。 这样即使集群中有某个节点挂掉了,因为大部分集群节点都加锁成功了,所以分布式锁还是可以继续使用的。

Redisson 中的 RedLock 是基于 RedissonMultiLock(联锁)实现的。

RedissonMultiLock 是 Redisson 提供的一种分布式锁类型,它可以同时操作多个锁,以达到对多个锁进行统一管理的目的。联锁的操作是原子性的,即要么全部锁住,要么全部解锁。这样可以保证多个锁的一致性。

20.哨兵模式

(自动选举老大的模式)

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。

哨兵模式专注于对 Redis 实例(主节点、从节点)运行状态的监控,并能够在主节点发生故障时通过一系列的机制实现选主及主从切换,实现故障自动转移和恢复,确保整个 Redis 系统的可用性,所以哨兵模式相比于主从同步多个一个自动容灾恢复的优点

如果master节点断开了,这个时候就会从从机中随机选择一个服务器(这里面是一个投票算法:

  1. num(total_sentinels)/2+1 即半数(所有哨兵数的半数,无论哨兵死活)以上的选票即可成为哨兵中的leader,这就是著名的Raft算法
  2. 选票数还必须大于等于quorum

如果断开的主机回来了也只能当新选出来的主机的从机

21.redis的大key问题

大key 通常指的是一个键包含了大量的数据,使得该键对应值的占用的内存超出了正常范围。

影响

  1. 内存消耗: 在进行缓存时降低缓存的效率,占用大量的内存空间,使得 Redis 的内存消耗急剧增加,还可能导致 Redis 实例的内存资源不足,甚至出发内存淘汰策略,从而影响系统的正常运行。
  2. 性能下降:由于 Redis 还是单线程的,处理 大key 的操作进而会阻塞其他请求的处理,从而影响系统性能。
  3. 持久化效率降低: 在进行持久化操作时,AOFRDB都会因为该 大key 耗费更多的时间,从而延迟持久化时间,分布式环境下甚至会造成缓存不一致。
  4. 网络传输延迟大key 在进行网络传输时会增加网络传输的延迟,在分布式环境下进行数据同步时可能会造成数据的不一致。

解决:

Mybatis

1.MyBatis 和 Hibernate有什么区别?

Hibernate 是一个全自动的 ORM 框架,不需要手动编写 SQL 语句,使用api进行数据库查询,学习成本相对较高。

MyBatis 是一个半自动的 ORM 框架,需要开发者手动编写和管理 SQL 语句,使用原生sql语句查询,可以更灵活地控制和优化 SQL 语句。

2.Mybatis的优缺点?

优点:

  1. 灵活:MyBatis 不会对应用程序或数据库的现有设计强加任何限制。它使用简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO 为数据库中的记录。
  2. 易于学习:MyBatis 相对于其他 ORM 框架来说,学习曲线较低,因为它不需要开发人员学习新的领域特定语言(DSL)或复杂的 API。
  3. 性能:MyBatis 可以通过缓存和批量更新等技术来提高性能。
  4. 可定制性:MyBatis 提供了许多插件和扩展点,例如 Plugin 扩展点,它是用于拦截 MyBatis 执行前后操作的接口,可以通过实现该接口来自定义 MyBatis 的行为,这样开发者就可以根据需要进行定制。

缺点:

  1. SQL 语句依赖:MyBatis 需要手动编写 SQL 语句,这意味着开发人员需要具备一定的 SQL 知识。此外,如果数据库模式发生变化,需要手动修改 SQL 语句,这可能会导致一些问题。
  2. XML 配置文件冗长:MyBatis 的配置文件通常比较冗长,这可能会导致一些维护问题。此外,如果使用注解配置,代码可能会变得混乱。
  3. 缺乏自动化创建:相比于其他 ORM 框架,MyBatis 缺乏自动化。例如,它不支持自动创建表和字段。

3.Mybatis 的执行流程

  1. 读取配置文件:读取 MyBatis 配置文件(通常是 mybatis-config.xml),该文件包含了 MyBatis 的全局配置信息,如数据库连接信息、类型别名、插件等。
  2. 创建 SqlSessionFactory:SqlSessionFactory 是 MyBatis 的核心接口之一,它负责创建 SqlSession 对象。SqlSessionFactory 可以通过 XML 配置文件或 Java 代码进行配置。
  3. 创建 SqlSession:SqlSession 是 MyBatis 的另一个核心接口,它负责与数据库进行交互。SqlSession 提供了许多方法,如 selectOne、selectList、insert、update、delete 等,可以执行 SQL 语句并返回结果。
  4. 执行 SQL 语句:SqlSession 会根据 Mapper 接口中的方法名和参数,找到对应的 SQL 语句并执行。在执行 SQL 语句之前,MyBatis 会将 #{}替换为实际的参数值,并将 ${} 替换为实际的 SQL 语句。
  5. 返回结果:执行 SQL 语句后,MyBatis 会将结果映射为 Java 对象并返回。MyBatis 提供了许多映射方式,如基于 XML 的映射、注解映射、自定义映射等。

4.#{}${}有什么区别?

  1. #{}用于预编译,提供参数安全性,适合大多数情况。
  2. ${}用于字符串替换,潜在安全风险较高,仅在特定情况下使用,确保参数值安全。

5.Mybatis一级二级缓存?

  1. 一级缓存是 SqlSession 级别的缓存,它的作用域是同一个 SqlSession,同一个 SqlSession 中的多次查询会共享同一个缓存。二级缓存是 Mapper 级别的缓存,它的作用域是同一个 Mapper,同一个 Mapper 中的多次查询会共享同一个缓存。
  2. 一级缓存是默认开启的,不需要手动配置。二级缓存需要手动配置,需要在 Mapper.xml 文件中添加 标签。
  3. 一级缓存的生命周期是和 SqlSession 一样长的,当 SqlSession 关闭时,一级缓存也会被清空。二级缓存的生命周期是和 MapperFactory 一样长的,当应用程序关闭时,二级缓存也会被清空。
  4. 一级缓存只能用于同一个 SqlSession 中的多次查询,不能用于跨 SqlSession 的查询。二级缓存可以用于跨 SqlSession 的查询,多个 SqlSession 可以共享同一个二级缓存。
  5. 一级缓存是线程私有的,不同的 SqlSession 之间的缓存数据不会互相干扰。二级缓存是线程共享的,多个 SqlSession 可以共享同一个二级缓存,需要考虑线程安全问题

6.Mybatis的核心组件有哪些?

  1. 首先第一个是,SqlSessionFactory,它就像是一个会话工厂。它的任务是创建 SqlSession 对象,这个对象是我们与数据库交互的主要途径。SqlSessionFactory 的作用很重要,因为它可以帮我们配置数据库连接信息和事务管理等。一旦这个工厂被建立起来,它就会加载一些必要的配置和映射文件,为后续的数据库操作提供一个可靠的基础。
  2. 第二个是 SqlSession,可以理解为我们与数据库进行互动的窗口。通过它,我们能够执行 SQL 语句,提交或回滚事务,还可以获取 Mapper 接口的实例。不过需要注意的是,SqlSession 的生命周期是短暂的,通常在数据库操作完成后就应该关闭它,这样可以释放资源。
  3. 接下来是 Mapper 接口,这个概念有点像定义了一套数据库操作的规则。每个 Mapper 接口对应一个或多个映射文件,里面的方法定义了具体的 SQL 操作,比如插入、更新、删除和查询等。MyBatis 通过动态代理的方式,把接口方法和映射文件中的 SQL 语句关联起来,这样我们就可以方便地通过接口来执行数据库操作。
  4. 最后,是==映射文件==,它是一个用来连接 Java 对象和数据库表的桥梁。在映射文件里,我们可以定义 SQL 语句、参数映射、结果映射等等。里面的 SQL 语句可以包括增删改查等操作,MyBatis 会根据我们调用的方法来选择正确的 SQL 语句来执行。

JVM

1.类加载器

类加载器(Class Loader)是 Java 虚拟机(JVM)的重要组成部分,负责将字节码文件加载到内存中并转换为可执行的类。

类加载器使用一种称为双亲委派模型(Parent Delegation Model)的机制来加载类。

类加载器主要有以下几种类型:

  1. 启动类加载器(Bootstrap ClassLoader):

    这是虚拟机自身的类加载器,负责加载JVM运行时环境所需的类库,如rt.jar等。它是C/C++实现的,因此在Java中无法直接获取到。

  2. 扩展类加载器(Extension ClassLoader):

    这个类加载器用来加载Java的扩展库,如$JAVA_HOME/lib/ext目录下的jar包。它是由Java编写的,在java.lang.ClassLoader的直接子类。

  3. 应用程序类加载器(Application ClassLoader):

    也称为系统类加载器,负责加载应用程序classpath下的类库。它是由Java编写的,在java.lang.ClassLoader的直接子类。

  4. 自定义类加载器(Custom ClassLoader):

    可以根据需求自定义的类加载器,继承自java.lang.ClassLoader。通过这种方式,可以实现一些特殊需求,如加载加密的类文件、从网络上动态下载类等。

2.双亲委派机制

双亲委派机制(Parent Delegation Mechanism)是Java中的一种类加载机制。在Java中,类加载器负责加载类的字节码并创建对应的Class对象。双亲委派机制是指当一个类加载器收到类加载请求时,它会先将该请求委派给它的父类加载器去尝试加载。只有当父类加载器无法加载该类时,子类加载器才会尝试加载。

这种机制的设计目的是为了保证类的加载是有序的,避免重复加载同一个类。Java中的类加载器形成了一个层次结构,根加载器(Bootstrap ClassLoader)位于最顶层,它负责加载Java核心类库。其他加载器如扩展类加载器(Extension ClassLoader)和应用程序类加载器(Application ClassLoader)都有各自的加载范围和职责。通过双亲委派机制,可以确保类在被加载时,先从上层的加载器开始查找,逐级向下,直到找到所需的类或者无法找到为止。

这种机制的好处是可以避免类的重复加载,提高了类加载的效率和安全性。同时,它也为Java提供了一种扩展机制,允许开发人员自定义类加载器,实现特定的加载策略。

优点

  1. 避免重复加载:通过委派给父类加载器,可以避免同一个类被多次加载,提高了加载效率。
  2. 安全性通过双亲委派机制,核心类库由根加载器加载,可以确保核心类库的安全性,防止恶意代码替换核心类。
  3. 扩展性:开发人员可以自定义类加载器,实现特定的加载策略,从而扩展Java的类加载机制。

缺点

  1. 灵活性受限:双亲委派机制对于某些特殊的类加载需求可能过于严格,限制了加载器的灵活性。
  2. 破坏隔离性:如果自定义类加载器不遵循双亲委派机制,可能会破坏类加载的隔离性,导致类冲突或安全性问题。
  3. 不适合动态更新:由于类加载器在加载类时会先检查父加载器是否已加载,因此在动态更新类时可能会出现问题,需要额外的处理。
  4. 总体而言,双亲委派机制通过层次结构和委派机制提供了一种有序、安全的类加载方式,但也存在一些限制和不适用的情况。

3.Native

凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层C语言的库。

被native关键字修饰的方法叫做本地方法,本地方法和其它方法不一样,本地方法意味着和平台有关,因此使用了native的程序可移植性都不太高。另外native方法在JVM中运行时数据区也和其它方法不一样,它有专门的本地方法栈。native方法主要用于加载文件和动态链接库,由于Java语言无法访问操作系统底层信息(比如:底层硬件设备等),这时候就需要借助C语言来完成了。被native修饰的方法可以被C语言重写。

JNI:Java Native Interface (java本地方法接口)

凡是带了native关键字的方法就会进入本地方法栈,其他的就是Java栈;

Native Interface 本地接口

本地接口的作用是融合不同的编程语言为Java所用,它的初衷及时融合C/C++程序,Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是在 Native Method Stack 中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java管理系统生产设备,在企业级应用中已经比较少见。因为现在的异构领域间通信很发达,比如可以使用Socket通信,也可以使用Web Service 等等,不多做介绍!

Native Method Stack

它的具体做法是Native Method Stack 中登记native方法,在(Execution Engine)执行引擎执行的时候加载Native Libraies。

4.JVM内存结构

image-20240825170554745

通常所说的 JVM 内存布局,一般指的是 JVM 运行时数据区(Runtime Data Area),也就是当字节码被类加载器加载之后的执行区域划分。

《Java虚拟机规范》中将 JVM 运行时数据区域划分为以下 5 部分:

  1. 程序计数器(Program Counter Register):用于记录当前线程执行的字节码指令地址,是线程私有的,线程切换不会影响程序计数器的值。

  2. Java 虚拟机栈(Java Virtual Machine Stacks):用于存储方法执行时的局部变量表、操作数栈、动态链接、方法出口等信息,也是线程私有的。每个方法在执行时都会创建一个栈帧,栈帧包含了方法的局部变量表、操作数栈等信息。

  3. 本地方法栈(Native Method Stack):与 Java 虚拟机栈类似,用于存储本地方法的信息。

  4. Java 堆(Java Heap):用于存储对象实例和数组,是 JVM 中最大的一块内存区域,它是所有线程共享的。堆通常被划分为年轻代和老年代,以支持垃圾回收机制。

    • 年轻代(Young Generation):用于存放新创建的对象。年轻代又分为 Eden 区和两个 Survivor 区(通常是一个 From 区和一个 To 区),对象首先被分配在 Eden 区,经过垃圾回收后存活的对象会被移到 Survivor 区,经过多次回收后仍然存活的对象会晋升到老年代。

    • 老年代(Old Generation):用于存放存活时间较长的对象。老年代主要存放长时间存活的对象或从年轻代晋升过来的对象。

  5. 方法区(Methed Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区也是所有线程共享的。

5.程序计数器的作用

PC 寄存器的作用包括:

  1. 指示当前线程执行的位置:PC 寄存器存储了当前线程正在执行的字节码指令的地址,因此可以用来确定线程的执行位置,以便于执行下一条指令。
  2. 实现线程切换:在多线程环境下,JVM 可以利用 PC 寄存器来实现线程切换。当一个线程被暂停执行时,JVM 可以保存该线程的 PC 寄存器状态,然后切换到另一个线程的 PC 寄存器,从而实现线程之间的切换和并发执行。
  3. 保存方法调用栈信息:PC 寄存器还可以用来保存方法调用栈信息,包括方法调用的返回地址等,以支持方法的嵌套调用和返回。

6.方法区

Method Area 方法区

方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享空间

静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在堆内存中,和方法区无关

jdk7之前常量池存在方法区中,jdk7之后常量池存在堆中

static、final、Class、常量池

7.栈

栈是线程级的

栈:栈内存,主管程序的运行,生命周期和线程同步

线程结束,栈内存也就释放,对于栈来说,不存在垃圾回收问题

一旦线程结束,栈就over

栈:八大基本类型、对象引用、局部变量、实例的方法

栈运行原理:栈帧

image-20240826163613933

8.堆

Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的。

堆内存中还要细分为三个区域:

GC 垃圾回收,主要是在新生区和老年区,

假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:Java heap space

在JDK8以后,永久存储区改了个名字(元空间,元空间在计算机的内存中,不在JVM中)

新生区

老年区

新生区一个对象经历了15次轻GC都活下来了,就会进入年老区

新生区中的伊甸园区,幸存区0,1都满了,就会进行重GC,活下来的对象进入老年区

永久区

这个区域常驻内存的。用来存在JDK自身携带的Class对象。Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收!关闭VM虚拟就会释放这个区域的内存

一个启动类,加载了大量的第三方jar包。Tomcat 部署了太多的应用,大量动态生成的反射类,不断的被加载,直到内存满,就会出现OOM

注意:方法区是一种逻辑概念,永久代和元空间都是对方法区的具体实现

逻辑上存在,物理上不存在

9.死亡对象判断方法

引用计数法和可达性分析法

引用计数法:

可达性分析法:

这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

10.GC(垃圾回收)

复制算法,标记清除法,标记压缩法。

1.复制算法

复制算法主要是用在新生区

复制算法最佳使用场景:对象存活度较低的时候;新生区~

image-20240901211451550

2.标记清除算法

image-20240826193945354

3.标记压缩法

image-20240901211507019

总结

内存效率:复制算法>标记清除算法>标记压缩算法(时间复杂度)

内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

思考一个问题:难道没有最优算法吗?

答案:没有,没有最好的算法,只有最合适的算法 -------> GC:分代收集算法

年轻代:

老年代:

11.什么是Full GC?

Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。

Full GC 通常发生在以下情况之一:

  1. 显式触发:通过调用 System.gc() 方法显式触发垃圾回收。虽然调用该方法只是向 JVM 发出建议,但在某些情况下,JVM 可能会选择执行 Full GC。
  2. 老年代空间不足:当老年代空间不足时,无法进行对象的分配,会触发 Full GC。此时,Full GC 的目标是回收老年代中的无效对象,以释放空间供新的对象分配。
  3. 永久代或元数据区空间不足:在使用永久代(Java 8 之前)或元数据区(Java 8 及之后)存储类的元数据信息时,如果空间不足,会触发 Full GC。

Full GC 是一种较为耗时的操作,因为它需要扫描和回收整个堆内存。在 Full GC 过程中,应用程序的执行通常会暂停,这可能会导致较长的停顿时间(长时间的停顿会影响应用程序的响应性能)。 为了避免频繁的 Full GC,通常采取一些优化措施,如合理设置堆大小、调优垃圾回收参数、减少对象的创建和存活时间等。

12.怎么进行JVM调优?

JVM 调优是一个很大的话题,在回答“如何进行 JVM 调优?”之前,首先我们要回答一个更为关键的问题,那就是,我们为什么要进行 JVM 调优?

只有知道了为什么要进行 JVM 调优之后,你才能准确的回答出来如何进行 JVM 调优?

要进行 JVM 调优无非就是以下两种情况:

  1. 目标驱动型的 JVM 调优,如,我们是为了最短的停顿时间所以要进行 JVM 调优,或者是我们为了最大吞吐量所以要进行 JVM 调优等。
  2. 问题驱动型的 JVM 调优,因为生产环境出现了频繁的 FullGC 了,导致程序执行变慢,所以我们要进行 JVM 调优。

所以,针对不同的 JVM 调优的手段和侧重点也是不同的。

总的来说,JVM 进行调优的流程如下:

  1. 确定 JVM 调优原因
  2. 分析 JVM(目前)运行情况
  3. 设置 JVM 调优参数
  4. 压测观测调优后的效果
  5. 应用调优后的配置

1.确定JVM调优原因

先确定是目标驱动型的 JVM 调优,还是问题驱动型的 JVM 调优。

如果是目标性的 JVM 调优,那么 JVM 调优实现思路就比较简单了,如:

  1. 以最短停顿时间为目标的调优,只需要将垃圾收集器设置成以最短停顿时间的为目标的垃圾收集器即可,如 CMS 收集器或 G1 收集器。
  2. 以吞吐量为目标的调优,只需要将垃圾收集器设置为 Parallel Scavenge 和 Parallel Old 这种以吞吐量为主要目标的垃圾回收器即可。

如果是以问题驱动的 JVM 调优,那就要先分析问题是什么,然后再进行下一步的调优了。

2.分析JVM运行情况

我们可以借助于目前主流的监控工具 Prometheus + Grafana 和 JDK 自带的命令行工具,如 jps、jstat、jinfo、jstack 等进行 JVM 运行情况的分析。

主要分析的点是 Young GC 和 Full GC 的频率,以及垃圾回收的执行时间。

3.设置JVM调优参数

常见的 JVM 调优参数有以下几个:

4.压测观测调优后的效果

JVM 参数调整之后,我们要通过压力测试来观察 JVM 参数调整前和调整后的差别,以确认调整后的效果。

5.应用调优后的配置

在确认了 JVM 参数调整后的效果满足需求之后,就可以将 JVM 的参数配置应用与生产环境了。

13.内存溢出和内存泄漏

内存溢出(Memory Overflow)和内存泄漏(Memory Leak)是两个与内存管理相关的问题,它们有以下区别:

  1. 定义不同
    1. 内存溢出指的是在程序运行过程中申请的内存超出了可用内存资源的情况,导致无法继续分配所需的内存,从而引发异常。
    2. 内存泄漏指的是在程序中无意中保留了不再需要的对象引用,导致这些对象无法被垃圾回收机制回收,进而占用了不必要的内存空间。
  2. 产生原因不同
    1. 内存溢出通常是由于程序运行时需要的内存超过了可用的内存资源,或者是存在大量占用内存的对象无法被及时释放。常见的内存溢出原因包括创建过多的对象、递归调用导致栈溢出等。
    2. 内存泄漏则是由于程序中存在不正确的对象引用管理,例如对象被误持有引用、缓存未清理等。
  3. 影响不同
    1. 内存溢出会导致程序抛出 OutOfMemoryError 异常,程序无法继续执行。
    2. 内存泄漏则会导致内存资源的浪费,长时间运行下会导致可用内存逐渐减少,最终可能导致内存溢出。
  4. 解决方案不同
    1. 对于内存溢出,可以通过增加可用内存、调整程序逻辑、优化资源使用等方式来解决。
    2. 而对于内存泄漏,需要通过检查和修复对象引用管理问题,确保不再使用的对象能够被垃圾回收机制正确释放。

14.为什么要使用元空间代替永久代?

相比于永久代,元空间具有更好的灵活性和扩展性,可以更好地满足不同应用程序的需求。 永久代的大小是固定的,当加载的类信息、常量池等数据超过了永久代的大小时,就会导致内存溢出。而元空间的大小可以根据需要进行调整,不再受到固定大小的限制。同时,元空间的数据可以存储在本地内存中,不再受到 Java 堆大小的限制。因此,使用元空间替代永久代可以提高程序的灵活性和稳定性。 所以,元空间的优势主要体现在以下两点:

  1. 提高稳定性,降低 OOM
  2. 降低运维成本

RabbitMQ

1.什么是 RabbitMQ?有什么显著的特点?

RabbitMQ 是一个开源的消息中间件,使用 Erlang 语言开发。这种语言天生非常适合分布式场景,RabbitMQ 也就非常适用于在分布式应用程序之间传递消息。

特点:

  1. 消息传递模式:RabbitMQ 支持多种消息传递模式,包括发布/订阅、点对点和工作队列等,使其更灵活适用于各种消息通信场景。
  2. 消息路由和交换机:RabbitMQ 引入了交换机(Exchange)的概念,用于将消息路由到一个或多个队列。这允许根据消息的内容、标签或路由键进行灵活的消息路由,从而实现更复杂的消息传递逻辑。
  3. 消息确认机制:RabbitMQ 支持消息确认机制,消费者可以确认已成功处理消息。这确保了消息不会在传递后被重复消费,增加了消息的可靠性。
  4. 可扩展性:RabbitMQ 是高度可扩展的,可以通过添加更多的节点和集群来增加吞吐量和可用性。这使得 RabbitMQ 适用于大规模的分布式系统。
  5. 多种编程语言支持:RabbitMQ 提供了多种客户端库和插件,支持多种编程语言,包括 Java、Python、Ruby、Node.js 等,使其在不同技术栈中都能方便地集成和使用。
  6. 消息持久性:RabbitMQ 允许消息和队列的持久性设置,确保消息在 RabbitMQ 重新启动后不会丢失。这对于关键的业务消息非常重要。
  7. 灵活的插件系统:RabbitMQ 具有丰富的插件系统,使其可以扩展功能,包括管理插件、数据复制插件、分布式部署插件等。
  8. 管理界面:RabbitMQ 提供了一个易于使用的 Web 管理界面,用于监视和管理队列、交换机、连接和用户权限等。

2.RabbitMQ和AMQP是什么关系?

AMQP: AMQP 不是一个具体的消息中间件产品,而是一个协议规范。他是一个开放的消息产地协议,是一种应用层的标准协议,为面向消息的中间件设计。AMQP 提供了一种统一的消息服务,使得不同程序之间可以通过消息队列进行通信。 SpringBoot 框架默认就提供了对 AMQP 协议的支持springAMQP。

RabbitMQ:RabbitMQ则是一个开源的消息中间件,是一个具体的软件产品。RabbitMQ 使用 AMQP 协议来实现消息传递的标准,但其实他也支持其他消息传递协议,如 STOMP 和 MQTT。RabbitMQ 基于 AMQP 协议定义的消息格式和交互流程,实现了消息在生产者、交换机、队列之间的传递和处理。

SpringAMQP的三个功能:

  1. 自动声明队列交换机及其绑定关系
  2. 基于注解的监听器模式
  3. 封装了RabbitTemplate工具

3.RabbitMQ核心组件有哪些?

  1. Broker:RabbitMQ服务器,负责接收和分发消息的应用。
  2. Virtual Host:虚拟主机,是RabbitMQ中的逻辑容器,用于隔离不同环境或不同应用程序的信息流。每个虚拟主机都有自己的队列、交换机等设置,可以理解为一个独立的RabbitMQ服务。
  3. Connection 连接:管理和维护与RabbitMQ服务器的TCP连接,生产者、消费者通过这个连接和 Broker 建立物理网络连接。
  4. Channel通道:是在Connection 内创建的轻量级通信通道,用于进行消息的传输和交互。应用程序通过Channel进行消息的发送和接收。通常一个 Connection 可以建立多个 Channel。
  5. Exchange交换机:交换机是消息的中转站,负责接收来自生产者的消息,并将其路由到一个或多个队列中。RabbitMQ 提供了多种不同类型的交换机,每种类型的交换机都有不同的消息路由规则。
  6. Queue队列:队列是消息的存储位置。每个队列都有一个唯一的名称。消息从交换机路由到队列,然后等待消费者来获取和处理。
  7. Binding绑定关系: Binding 是 Exchange 和 Queue 之间的关联规则,定义了消息如何从交换机路由到特定的队列。

4.RabbitMQ 中有哪几种交换机类型?

  1. Fanout广播

  2. Direct订阅

  3. Topic通配符订阅

  4. Headers头匹配

交换机主要作用:

  1. 接受publisher的消息
  2. 将消息按规则路由到队列
  3. 不能缓存消息

topic模式消费者代码演示:

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue1"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "china.#"
))
public void listenTopicQueue1(String msg){
    System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}

@RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "topic.queue2"),
    exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
    key = "#.news"
))
public void listenTopicQueue2(String msg){
    System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}

5.配置MQ

不管是生产者还是消费者,都需要配置MQ的基本信息。分为两步:

  1. 添加依赖:
  <!--消息发送-->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
  </dependency>
  1. 配置MQ地址:
spring:
  rabbitmq:
    host: 192.168.150.101 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

6.RabbitMQ 支持哪些消息模式?

  1. workQueue 工作序列机制: Producer 将消息发送到 queue,多个 Consumer 同时消费Queue 上的消息。消息会均匀的分配给多个 Consumer 处理。
  2. Publish/Subscribe 订阅发布机制: Producer 只负责将消息发送到exchange交换机上。Exchange 将消息转发到所有订阅的 Queue,并由对应的 Consumer 去进行消费
  3. Routing 基于内容路由机制:在订阅发布机制的基础上,增加一个routingKey,并根据routingKey判断 Exchange 将消息转发到哪些 Queue 上。
  4. Topic 基于话题路由机制:在基于内容路由的基础上,对routingKey增加了模糊匹配的功能。

7.如何保证MQ消息的可靠性?

  1. 生产者可靠性
  2. MQ可靠性
  3. 消费者可靠性

生产者可靠性

①生产者重试机制

如网络故障情况,SpringAMQP提供的消息发送时的重试机制,当RabbitTemplate与MQ连接超时后,多次重试。但是该机制是阻塞式的重试,所以大多禁用该机制。

如何开启?:

修改publisher模块的application.yaml文件,添加下面的内容:

spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数

②生产者确认机制

在publisher模块的application.yaml中添加配置:

spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

publisher-confirm-type参数:

none:关闭确认机制

simple:同步阻塞等mq回执

correlated:异步回调返回回执

触发确认的几种情况:

MQ可靠性

①数据持久化

交换机和队列持久化开启方式:

durable:true

消息持久化开启方式:

deliveryMode=2

②LazyQueue

开启:

x-queue-mode=lazy

消费者可靠性

①消费者确认机制

acknowledge-mode=auto

②失败重试机制

消息会不断入队

③失败处理策略

④业务幂等性

设唯一消息id

8.死信队列

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

如果一个队列中的消息已经成为死信,并且这个队列通过**dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机就称为死信交换机**(Dead Letter Exchange)。而此时加入有队列与死信交换机绑定,则最终死信就会被投递到这个队列中。


计算机网络

1.OSI七层模型?TCP/IP 四层模型?

OSI七层模型:

image-20240828155843420

TCP/IP 四层模型:

  1. 应用层
  2. 传输层
  3. 网络层
  4. 网络接口层

2.各层常见协议?

应用层常见协议

image-20240828160054990

传输层常见协议

image-20240828160117154

网络成常见协议

image-20240828160129113

3.从输入url到页面展示其中发生了什么?

  1. 在浏览器中输入指定网页的 URL。
  2. 浏览器通过 DNS 协议,获取域名对应的 IP 地址。
  3. 浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求。
  4. 浏览器在 TCP 连接上,向服务器发送一个 HTTP 请求报文,请求获取网页的内容。
  5. 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
  6. 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
  7. 浏览器在不需要和服务器通信时,可以主动关闭 TCP 连接,或者等待服务器的关闭请求。

4.HTTP大致状态码

image-20240828160404342

5.http和https有什么区别?

HTTPS就是在HTTP和TCP层之间加了SSL/TLS安全传输层

  1. 端口号:HTTP 默认是 80,HTTPS 默认是 443。
  2. URL 前缀:HTTP 的 URL 前缀是 http://,HTTPS 的 URL 前缀是 https://
  3. 安全性和资源消耗:HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。
  4. SEO(搜索引擎优化):搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。

6.HTTP是保存状态的协议,如何保存用户状态?

HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们如何保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。

在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。

Cookie 被禁用怎么办?

最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。

7.GET和POST的区别?

  1. 语义(主要区别):GET 通常用于获取或查询资源,而 POST 通常用于创建或修改资源
  2. 幂等GET 请求是幂等的,即多次重复执行不会改变资源的状态,而 POST 请求是不幂等的,即每次执行可能会产生不同的结果或影响资源的状态。
  3. 格式GET 请求的参数通常放在 URL 中,形成查询字符串(querystring),而 POST 请求的参数通常放在请求体(body)中,可以有多种编码格式,如 application/x-www-form-urlencoded、multipart/form-data、application/json 等。GET 请求的 URL 长度受到浏览器和服务器的限制,而 POST 请求的 body 大小则没有明确的限制。不过,实际上 GET 请求也可以用 body 传输数据,只是并不推荐这样做,因为这样可能会导致一些兼容性或者语义上的问题。
  4. 缓存:由于 GET 请求是幂等的,它可以被浏览器或其他中间节点(如代理、网关)缓存起来,以提高性能和效率。而 POST 请求则不适合被缓存,因为它可能有副作用,每次执行可能需要实时的响应。
  5. 安全性:GET 请求和 POST 请求如果使用 HTTP 协议的话,那都不安全,因为 HTTP 协议本身是明文传输的,必须使用 HTTPS 协议来加密传输数据。另外,GET 请求相比 POST 请求更容易泄露敏感数据,因为 GET 请求的参数通常放在 URL 中

8.WebSocket

什么是WebSocket?

WebSocket 是一种基于 TCP 连接的全双工通信协议,即客户端和服务器可以同时发送和接收数据。

WebSocket 协议本质上是应用层的协议,用于弥补 HTTP 协议在持久通信能力上的不足。客户端和服务器仅需一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

WebSocket 和 HTTP 有什么区别?

  1. WebSocket 是一种双向实时通信协议,而 HTTP 是一种单向通信协议。并且,HTTP 协议下的通信只能由客户端发起,服务器无法主动通知客户端。
  2. WebSocket 使用 ws:// 或 wss://(使用 SSL/TLS 加密后的协议,类似于 HTTP 和 HTTPS 的关系) 作为协议前缀,HTTP 使用 http:// 或 https:// 作为协议前缀。
  3. WebSocket 可以支持扩展,用户可以扩展协议,实现部分自定义的子协议,如支持压缩、加密等。
  4. WebSocket 通信数据格式比较轻量,用于协议控制的数据包头部相对较小,网络开销小,而 HTTP 通信每次都要携带完整的头部,网络开销较大(HTTP/2.0 使用二进制帧进行数据传输,还支持头部压缩,减少了网络开销)。

WebSocket的工作过程?

  1. 客户端向服务器发送一个 HTTP 请求,请求头中包含 Upgrade: websocketSec-WebSocket-Key 等字段,表示要求升级协议为 WebSocket;
  2. 服务器收到这个请求后,会进行升级协议的操作,如果支持 WebSocket,它将回复一个 HTTP 101 状态码,响应头中包含 ,Connection: UpgradeSec-WebSocket-Accept: xxx 等字段、表示成功升级到 WebSocket 协议。
  3. 客户端和服务器之间建立了一个 WebSocket 连接,可以进行双向的数据传输。数据以帧(frames)的形式进行传送,WebSocket 的每条消息可能会被切分成多个数据帧(最小单位)。发送端会将消息切割成多个帧发送给接收端,接收端接收消息帧,并将关联的帧重新组装成完整的消息。
  4. 客户端或服务器可以主动发送一个关闭帧,表示要断开连接。另一方收到后,也会回复一个关闭帧,然后双方关闭 TCP 连接。

9.DNS执行流程是什么?

DNS(Domain Name System)域名管理系统。DNS 要解决的是域名和 IP 地址的映射问题

  1. 浏览器输入域名后先查浏览器缓存
  2. 没有的话查操作系统缓存
  3. 没有的话查hosts文件
  4. 缓存都没有再去问本地DNS服务器
  5. 本地DNS服务器没有的话会去问根DNS服务器,根DNS服务器会指出对应的顶级域名服务器让本地服务器去问
  6. 顶级域名服务器会指出权威DNS服务器让本地DNS服务器去问
  7. 权威DNS服务器解析出域名对应的ip地址告诉本地DNS
  8. 本地DNS服务器把ip地址告诉客户端

大致:浏览器缓存——操作系统缓存——hosts文件——本地DNS——根DNS——顶级DNS——权威DNS——本地DNS——客户端

10.TCP与UDP区别?

image-20240828162012335

什么时候选TCP什么时候选UDP?

UDP 一般用于即时通信,比如:语音、 视频、直播等等。这些场景对传输数据的准确性要求不是特别高,比如你看视频即使少个一两帧,实际给人的感觉区别也不大。

TCP 用于对传输准确性要求特别高的场景,比如文件传输、发送和接收邮件、远程登录等等。

11.TCP三次握手四次挥手

第三次握手是可以携带数据的,前两次握手是不可以携带数据的

三次握手:

一次握手:客户端发送带有 SYN(SEQ=x) 标志的数据包 -> 服务端,等待服务端的确认;

二次握手:服务端发送带有 SYN+ACK(SEQ=y,ACK=x+1) 标志的数据包 –> 客户端

三次握手:客户端发送带有 ACK(ACK=y+1) 标志的数据包 –> 服务端,然后客户端和服务端都进入TCP连接成功状态,完成 TCP 三次握手。

四次挥手:

第一次挥手:客户端发送一个 FIN(SEQ=x) 标志的数据包->服务端,用来关闭客户端到服务端的数据传送。然后客户端进入 FIN-WAIT-1 状态。

第二次挥手:服务端收到这个 FIN(SEQ=X) 标志的数据包,它发送一个 ACK (ACK=x+1)标志的数据包->客户端 。然后服务端进入 CLOSE-WAIT 状态,客户端进入 FIN-WAIT-2 状态。

第三次挥手:服务端发送一个 FIN (SEQ=y)标志的数据包->客户端,请求关闭连接,然后服务端进入 LAST-ACK 状态。

第四次挥手:客户端发送 ACK (ACK=y+1)标志的数据包->服务端,然后客户端进入超时等待时间,服务端在收到 ACK (ACK=y+1)标志的数据包后进入 CLOSE 状态。此时如果客户端等待 2MSL 后依然没有收到回复,就证明服务端已正常关闭,随后客户端也可以关闭连接了。

12.三次握手的目的?

保证双方都有发送和接收的能力

13.为什么网络要分层?

  1. 各层之间相互独立:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)。这个和我们对开发时系统进行分层是一个道理。
  2. 提高了灵活性和可替换性:每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。并且,每一层都可以根据需要进行修改或替换,而不会影响到整个网络的结构。这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。
  3. 大问题化小:分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。

14.https执行流程

  1. 客户端使用 HTTPS 访问服务器端。
  2. 服务器端返回数字证书,以及使用非对称加密(概念见最下方),生成一个公钥给客户端(私钥服务器端自己保留)。
  3. 客户端验证数字证书是否有效,如果无效,终止访问,如果有效:
    1. 使用对称加密(概念见最下方)生成一个共享秘钥;
    2. 使用对称加密的共享秘钥加密数据;
    3. 使用非对称加密的公钥加密(对称加密生成的)共享秘钥。
    4. 发送加密后的秘钥和数据给服务器端。
  4. 服务器端使用私钥解密出客户端(使用对称加密生成的)共享秘钥,再使用共享秘钥解密出数据的具体内容。
  5. 之后客户端和服务器端就使用共享秘钥加密的内容内容进行交互了。

15.HTTP2.0和3.0有什么区别:

  1. 2.0是基于TCP协议实现的,3.0新增了QUIC协议,提供与 TLS/SSL 相当的安全性,具有较低的连接和传输延迟。
  2. 2.0需要经过经典的三次握手建立连接,3.0连接建立仅需 0-RTT 或者 1-RTT。这意味着 QUIC 在最佳情况下不需要任何的额外往返时间就可以建立新连接。
  3. HTTP/2.0 使用 HPACK 算法进行头部压缩,而 HTTP/3.0 使用更高效的 QPACK 头压缩算法。
  4. HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。HTTP/3.0 一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。

HTTP/2.0:使用 TCP 作为传输协议、使用 HPACK 进行头部压缩、依赖 TLS 进行加密。

HTTP/3.0:使用基于 UDP 的 QUIC 协议、使用更高效的 QPACK 进行头部压缩、在 QUIC 中直接集成了 TLS。QUIC 协议具备连接迁移、拥塞控制与避免、流量控制等特性。

16.TCP流量控制是怎么实现的?

通过==滑动窗口==机制实现

image-20240902223339680

17.TCP拥塞控制怎么实现的

通过四个

  1. 慢开始:指数增长
  2. 拥塞避免:+1增长
  3. 快重传:收到三个重复确认
  4. 快恢复:÷2

image-20240902225357753

18.TCP如何保证可靠传输?

  1. 首部校验和,如果收到的段的校验和有差错,则丢弃该段

  2. 对失序数据包重排序以及去重

  3. 重传机制

    1. 超时重传(基于计时器重传),

    2. 快重传(连续收到三个重复确认),

    3. SACK (可以将已收到的数据的信息发送给「发送方」,只重传丢失的数据,

    4. D SACK (使用了 SACK 来告诉「发送方」有哪些数据被重复接收了)

  4. 流量控制(滑动窗口机制)

  5. 拥塞控制(慢开始,拥塞避免,快重传,快恢复)

19.SSL/TLS协议基本流程?

  1. 客户端向服务端索要并验证服务器公钥
  2. 双方协商生产【会话密钥】
  3. 双方采用【会话密钥】进行加密通信

1.2步是TLS握手阶段

20.HTTPS采用的加密方式:

对称加密和非对称加密结合的”混合加密“

21.ARP协议(地址解析协议)

根据ip地址查询相应的MAC地址

怎么查?

答:ARP 协议会在以太网中以广播的形式,对以太网所有的设备喊出:“这个 IP 地址是谁的?请把你的 MAC 地址告诉我”。

然后就会有人回答:“这个 IP 地址是我的,我的 MAC 地址是 XXXX”。

好像每次都要广播获取,这不是很麻烦吗?

答:后续操作系统会把本次查询结果放到一块叫做 ARP 缓存的内存空间留着以后用,不过缓存的时间就几分钟。

22.网卡

网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方。因此,我们需要将**数字信息转换为电信号**,才能在网线上传输。

负责执行这一操作的是网卡,要控制网卡还需要靠网卡驱动程序

网卡驱动获取网络包之后,会将其复制到网卡内的缓存区中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列

image-20240903173644330

23.交换机

交换机里的模块将电信号转换为数字信号。

通过包末尾的 FCS 校验错误,如果没问题则放到缓冲区。

交换机的端口不具有 MAC 地址

交换机的 MAC 地址表主要包含两个信息:

image-20240903174103549

24.路由器

路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方;同时还具有 IP 地址

总的来说,路由器的端口都具有 MAC 地址,只接收与自身地址匹配的包,遇到不匹配的包则直接丢弃。

接下来,路由器会根据 MAC 头部后方的 IP 头部中的内容进行包的转发操作。

在网络包传输的过程中,源 IP 和目标 IP 始终是不会变的,一直变化的是 MAC 地址,因为需要 MAC 地址在以太网内进行两个设备之间的包传输。

25.HTTP是什么?

HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

26.HTTP 缓存技术

HTTP 缓存有两种实现方式,分别是强制缓存和协商缓存

强制缓存:

强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。

协商缓存:

协商缓存就是与服务端协商之后,通过协商结果来判断是否使用本地缓存。

27.HTTPS一定安全可靠吗?

问题场景:

客户端通过浏览器向服务端发起 HTTPS 请求时,被「假基站」转发到了一个「中间人服务器」,于是客户端是和「中间人服务器」完成了 TLS 握手,然后这个「中间人服务器」再与真正的服务端完成 TLS 握手。

但是要发生这种场景是有前提的,前提是用户点击接受了中间人服务器的证书。

中间人服务器与客户端在 TLS 握手过程中,实际上发送了自己伪造的证书给浏览器,而这个伪造的证书是能被浏览器(客户端)识别出是非法的,于是就会提醒用户该证书存在问题。

所以,HTTPS 协议本身到目前为止还是没有任何漏洞的,即使你成功进行中间人攻击,本质上是利用了客户端的漏洞(用户点击继续访问或者被恶意导入伪造的根证书),并不是 HTTPS 不够安全

28.HTTP/1.1相比HTTP/1.0有什么提升?

但 HTTP/1.1 还是有性能瓶颈:

29.HTTP/2 相比 HTTP/1.1 性能上的改进

30.HTTP/2的队头阻塞问题

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。

31.HTTP/3做了哪些优化?

使用QUIC协议,优点:

32.TCP四元组

TCP 四元组可以唯一的确定一个连接,四元组包括如下:

image-20240903180053511

33.HTTP/1.1 如何优化?

image-20240903181703618

34.HPACK头部压缩算法

HPACK 算法主要包含三个组成部分:

客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 ==Huffman 编码==压缩数据,可达到 50%~90% 的高压缩率

35.既然有 HTTP 协议,为什么还要有 RPC?

其实,TCP70年代出来的协议,而 HTTP90 年代才开始流行的。而直接使用裸 TCP 会有问题,可想而知,这中间这么多年有多少自定义的协议,而这里面就有80年代出来的 RPC

所以我们该问的不是既然有 HTTP 协议为什么要有 RPC,而是为什么有 RPC 还要有 HTTP 协议

HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。

36.既然有 HTTP 协议,为什么还要有 WebSocket?

37.TCP头格式有哪些?

源端口和目的端口

序列号

滑动窗口大小

校验和

首部长度

控制位:SYN ACK FIN RST(出现异常必须强制断开连接)

image-20240903184910629

38.TCP最大连接数

image-20240903185200390

当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:

39.UDP头部

image-20240903185352694

40.什么是 SYN 攻击?如何避免 SYN 攻击?

攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到一个 SYN 报文,就进入SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的 ACK 应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。

避免 SYN 攻击方式,可以有以下四种方法:

41.全连接队列和半连接队列

正常流程:

42.如果已经建立了连接,但是客户端突然出现故障了怎么办?

客户端出现故障指的是客户端的主机发生了宕机,或者断电的场景。发生这种情况的时候,如果服务端一直不会发送数据给客户端,那么服务端是永远无法感知到客户端宕机这个事件的,也就是服务端的 TCP 连接将一直处于 ESTABLISH 状态,占用着系统资源。

为了避免这种情况,TCP 搞了个保活机制

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

43.超时重传时间RTO

超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

44.如何解决粘包?

45.SYN报文什么情况下会被丢弃?

46.已建立的TCP连接收到SYN包会发生什么?

处于 Established 状态的服务端,如果收到了客户端的 SYN 报文(注意此时的 SYN 报文其实是乱序的,因为 SYN 报文的初始化序列号其实是一个随机数),会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 被称之为 Challenge ACK。

接着,客户端收到这个 Challenge ACK,发现确认号(ack num)并不是自己期望收到的,于是就会回 RST 报文,服务端收到后,就会释放掉该连接。

47.TCP和UDP可以绑定相同端口吗?

TCP 和 UDP 是两种不同的传输层协议,理论上它们是可以同时绑定到相同的端口的。因为 TCP 和 UDP 使用的端口号在操作系统中是相互独立的,两者的套接字由 (IP 地址, 端口号, 协议) 组成。因此,TCP 和 UDP 可以在同一个 IP 地址和端口号上并存,但它们的协议类型不同,这使得操作系统能够区分它们。

48.ping的工作原理

依靠ICMP协议,ICMP:用于告知网络包传送过程中产生的错误和各种控制信息

49.localhost和127.0.0.1

java基础

并发编程

Spring

MySQL数据库

Redis

Mybatis

JVM

RabbitMQ

计算机网络

算法题